import React from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act } from 'react'
import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import UploadWizard from '../UploadWizard'

function installAxiosStubs({ statusValue = 'ready', initError = null, holdChunk = false, finishError = null } = {}) {
  window.axios = {
    post: vi.fn((url, payload, config = {}) => {
      if (url === '/api/uploads/init') {
        if (initError) return Promise.reject(initError)
        return Promise.resolve({
          data: {
            session_id: 'session-1',
            upload_token: 'token-1',
          },
        })
      }

      if (url === '/api/uploads/chunk') {
        if (holdChunk) {
          return new Promise((resolve, reject) => {
            if (config?.signal?.aborted) {
              reject({ name: 'CanceledError', code: 'ERR_CANCELED' })
              return
            }
            config?.signal?.addEventListener?.('abort', () => reject({ name: 'CanceledError', code: 'ERR_CANCELED' }))
            setTimeout(() => resolve({ data: { received_bytes: 1024, progress: 55 } }), 20)
          })
        }

        const offset = Number(payload?.get?.('offset') || 0)
        const chunkSize = Number(payload?.get?.('chunk_size') || 0)
        const totalSize = Number(payload?.get?.('total_size') || 1)
        const received = Math.min(totalSize, offset + chunkSize)

        return Promise.resolve({
          data: {
            received_bytes: received,
            progress: Math.round((received / totalSize) * 100),
          },
        })
      }

      if (url === '/api/uploads/finish') {
        if (finishError) return Promise.reject(finishError)
        return Promise.resolve({ data: { processing_state: statusValue, status: statusValue } })
      }

      if (/^\/api\/uploads\/[^/]+\/publish$/.test(url)) {
        return Promise.resolve({ data: { success: true, status: 'published' } })
      }

      if (url === '/api/uploads/cancel') {
        return Promise.resolve({ data: { success: true, status: 'cancelled' } })
      }

      return Promise.reject(new Error(`Unhandled POST ${url}`))
    }),
    get: vi.fn((url) => {
      if (url === '/api/uploads/status/session-1') {
        return Promise.resolve({
          data: {
            id: 'session-1',
            processing_state: statusValue,
            status: statusValue,
          },
        })
      }

      if (url === '/api/tags/popular' || String(url).startsWith('/api/tags/search')) {
        return Promise.resolve({
          data: {
            data: [],
          },
        })
      }

      return Promise.reject(new Error(`Unhandled GET ${url}`))
    }),
  }
}

async function flushUi() {
  await act(async () => {
    await new Promise((resolve) => window.setTimeout(resolve, 0))
  })
}

async function renderWizard(props = {}) {
  await act(async () => {
    render(<UploadWizard {...props} />)
  })
  await flushUi()
}

async function uploadPrimary(file) {
  await act(async () => {
    const input = screen.getByLabelText('Upload file input')
    await userEvent.upload(input, file)
  })
  await flushUi()
}

async function uploadScreenshot(file) {
  await act(async () => {
    const input = await screen.findByLabelText('Screenshot file input')
    await userEvent.upload(input, file)
  })
  await flushUi()
}

async function completeStep1ToReady() {
  await uploadPrimary(new File(['img'], 'ready.png', { type: 'image/png' }))
  await act(async () => {
    await userEvent.click(await screen.findByRole('button', { name: /start upload/i }))
  })
  await waitFor(() => {
    expect(screen.getByRole('button', { name: /continue to publish/i })).not.toBeNull()
  })
}

async function completeRequiredDetails({ title = 'My Art', mature = false } = {}) {
  await act(async () => {
    await userEvent.type(screen.getByRole('textbox', { name: /title/i }), title)
    await userEvent.click(screen.getByRole('button', { name: /art .* open/i }))
    await userEvent.click(await screen.findByRole('button', { name: /root .* choose/i }))
    await userEvent.click(await screen.findByRole('button', { name: /sub .* choose/i }))
    await userEvent.type(screen.getByLabelText(/search or add tags/i), 'fantasy{enter}')
    if (mature) {
      await userEvent.click(screen.getByLabelText(/mark this artwork as mature content/i))
    }
    await userEvent.click(screen.getByLabelText(/i confirm i own the rights to this content/i))
  })
}

describe('UploadWizard step flow', () => {
  let originalImage
  let originalScrollTo
  let originalScrollIntoView
  let consoleErrorSpy

  beforeEach(() => {
    window.URL.createObjectURL = vi.fn(() => `blob:${Math.random().toString(16).slice(2)}`)
    window.URL.revokeObjectURL = vi.fn()

    originalImage = global.Image
    originalScrollTo = window.scrollTo
    originalScrollIntoView = Element.prototype.scrollIntoView
    window.scrollTo = vi.fn()
    Element.prototype.scrollIntoView = vi.fn()
    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
      const text = args.map((arg) => String(arg)).join(' ')
      if (text.includes('not configured to support act')) return
      if (text.includes('not wrapped in act')) return
      console.warn(...args)
    })
    global.Image = class MockImage {
      set src(_value) {
        this.naturalWidth = 1920
        this.naturalHeight = 1080
        setTimeout(() => {
          if (typeof this.onload === 'function') this.onload()
        }, 0)
      }
    }
  })

  afterEach(() => {
    global.Image = originalImage
    window.scrollTo = originalScrollTo
    Element.prototype.scrollIntoView = originalScrollIntoView
    consoleErrorSpy?.mockRestore()
    cleanup()
    vi.restoreAllMocks()
  })

  it('renders 3-step stepper', () => {
    installAxiosStubs()
    return renderWizard({ initialDraftId: 301 }).then(() => {
      expect(screen.getByRole('navigation', { name: /upload steps/i })).not.toBeNull()
      expect(screen.getByRole('button', { name: /1 upload/i })).not.toBeNull()
      expect(screen.getByRole('button', { name: /2 details/i })).not.toBeNull()
      expect(screen.getByRole('button', { name: /3 publish/i })).not.toBeNull()
    })
  })

  it('marks locked steps with aria-disabled and blocks click', async () => {
    installAxiosStubs()
    await renderWizard({ initialDraftId: 307 })

    const stepper = screen.getByRole('navigation', { name: /upload steps/i })
    const detailsStep = within(stepper).getByRole('button', { name: /2 details/i })
    const publishStep = within(stepper).getByRole('button', { name: /3 publish/i })

    expect(detailsStep.getAttribute('aria-disabled')).toBe('true')
    expect(publishStep.getAttribute('aria-disabled')).toBe('true')

    await act(async () => {
      await userEvent.click(detailsStep)
    })
    expect(screen.getByRole('heading', { level: 2, name: /upload your artwork/i })).not.toBeNull()
    expect(screen.queryByText(/add details/i)).toBeNull()
  })

  it('keeps step 2 hidden until step 1 upload is ready', async () => {
    installAxiosStubs({ statusValue: 'processing' })
    await renderWizard({ initialDraftId: 302 })

    expect(screen.queryByText(/artwork details/i)).toBeNull()

    await uploadPrimary(new File(['img'], 'x.png', { type: 'image/png' }))
    await act(async () => {
      await userEvent.click(await screen.findByRole('button', { name: /start upload/i }))
    })

    await waitFor(() => {
      expect(screen.queryByRole('button', { name: /continue to publish/i })).toBeNull()
    })
    expect(screen.queryByText(/artwork details/i)).toBeNull()
  })

  it('requires archive screenshot before start upload enables', async () => {
    installAxiosStubs()
    await renderWizard({ initialDraftId: 303 })

    await uploadPrimary(new File(['zip'], 'bundle.zip', { type: 'application/zip' }))

    const start = await screen.findByRole('button', { name: /start upload/i })
    await waitFor(() => {
      expect(start.disabled).toBe(true)
    })

    await uploadScreenshot(new File(['shot'], 'screen.png', { type: 'image/png' }))

    await waitFor(() => {
      expect(start.disabled).toBe(false)
    })
  })

  it('uses the selected archive screenshot as the preview upload source', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 312 })

    await uploadPrimary(new File(['zip'], 'bundle.zip', { type: 'application/zip' }))
    await uploadScreenshot(new File(['shot-1'], 'shot-1.png', { type: 'image/png' }))
    await uploadScreenshot(new File(['shot-2'], 'shot-2.png', { type: 'image/png' }))

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /use shot-2\.png as default screenshot/i }))
    })

    await waitFor(() => {
      expect(screen.getAllByText('Default').length).toBeGreaterThan(0)
    })

    await act(async () => {
      await userEvent.click(await screen.findByRole('button', { name: /start upload/i }))
    })

    await waitFor(() => {
      expect(window.axios.post).toHaveBeenCalledWith(
        '/api/uploads/finish',
        expect.objectContaining({
          file_name: 'shot-2.png',
          archive_file_name: 'bundle.zip',
          additional_screenshot_sessions: [
            expect.objectContaining({
              file_name: 'shot-1.png',
            }),
          ],
        }),
        expect.anything(),
      )
    })
  })

  it('allows navigation back to completed previous step', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 304 })

    await completeStep1ToReady()
    expect(await screen.findByText(/artwork details/i)).not.toBeNull()

    const stepper = screen.getByRole('navigation', { name: /upload steps/i })
    await act(async () => {
      await userEvent.click(within(stepper).getByRole('button', { name: /upload/i }))
    })
    expect(await screen.findByRole('heading', { level: 2, name: /upload your artwork/i })).not.toBeNull()
  })

  it('triggers scroll-to-top behavior on step change', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 308 })

    const scrollSpy = Element.prototype.scrollIntoView
    const initialCalls = scrollSpy.mock.calls.length

    await completeStep1ToReady()

    await waitFor(() => {
      expect(scrollSpy.mock.calls.length).toBeGreaterThan(initialCalls)
    })
  })

  it('shows publish only on step 3 and only after ready_to_publish path', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 305, contentTypes: [{ id: 1, name: 'Art', categories: [{ id: 10, name: 'Root', children: [{ id: 11, name: 'Sub' }] }] }] })

    await completeStep1ToReady()
    expect(await screen.findByText(/artwork details/i)).not.toBeNull()

    expect(screen.queryByRole('button', { name: /^publish$/i })).toBeNull()

    await completeRequiredDetails({ title: 'My Art' })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /continue to publish/i }))
    })

    await waitFor(() => {
      const publish = screen.getByRole('button', { name: /^publish$/i })
      expect(publish.disabled).toBe(false)
    })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /^publish$/i }))
    })

    await waitFor(() => {
      expect(screen.getByText(/your artwork is live/i)).not.toBeNull()
    })

    expect(window.axios.post).not.toHaveBeenCalledWith('/api/uploads/session-1/publish', expect.anything(), expect.anything())
  })

  it('includes the mature flag in the final publish payload when selected', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 311, contentTypes: [{ id: 1, name: 'Art', categories: [{ id: 10, name: 'Root', children: [{ id: 11, name: 'Sub' }] }] }] })

    await completeStep1ToReady()

    await screen.findByText(/artwork details/i)

    await completeRequiredDetails({ title: 'Mature Piece', mature: true })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /continue to publish/i }))
    })

    await waitFor(() => {
      const publish = screen.getByRole('button', { name: /^publish$/i })
      expect(publish.disabled).toBe(false)
    })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /^publish$/i }))
    })

    await waitFor(() => {
      expect(window.axios.post).toHaveBeenCalledWith(
        '/api/uploads/311/publish',
        expect.objectContaining({ is_mature: true }),
        expect.anything(),
      )
    })
  })

  it('includes contributor credit metadata in the final publish payload', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({
      initialDraftId: 313,
      currentUserId: 11,
      initialGroupSlug: 'warp-collective',
      groupOptions: [{ slug: 'warp-collective', name: 'Warp Collective' }],
      contributorOptionsByGroup: {
        'warp-collective': [
          { id: 10, name: 'Owner User', username: 'owner-user' },
          { id: 11, name: 'Editor User', username: 'editor-user' },
        ],
      },
      contentTypes: [{ id: 1, name: 'Art', categories: [{ id: 10, name: 'Root', children: [{ id: 11, name: 'Sub' }] }] }],
    })

    await completeStep1ToReady()
    await screen.findByText(/artwork details/i)

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /add credit/i }))
      await userEvent.type(screen.getByLabelText(/credit role for owner user/i), 'Color assist')
      await userEvent.click(screen.getByRole('button', { name: /mark owner user as lead supporting credit/i }))
    })

    await completeRequiredDetails({ title: 'Collaborative Piece' })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /continue to publish/i }))
    })

    await waitFor(() => {
      const publish = screen.getByRole('button', { name: /^publish$/i })
      expect(publish.disabled).toBe(false)
    })

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /^publish$/i }))
    })

    await waitFor(() => {
      expect(window.axios.post).toHaveBeenCalledWith(
        '/api/uploads/313/publish',
        expect.objectContaining({
          contributor_user_ids: [10],
          contributor_credits: [
            expect.objectContaining({
              user_id: 10,
              credit_role: 'Color assist',
              is_primary: true,
            }),
          ],
        }),
        expect.anything(),
      )
    })
  })

  it('shows personal and group publish options when group publishing is available', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({
      initialDraftId: 314,
      currentUserId: 11,
      groupOptions: [{ slug: 'warp-collective', name: 'Warp Collective' }],
      contributorOptionsByGroup: {
        'warp-collective': [
          { id: 11, name: 'Editor User', username: 'editor-user' },
          { id: 12, name: 'Owner User', username: 'owner-user' },
        ],
      },
      contentTypes: [{ id: 1, name: 'Art', categories: [{ id: 10, name: 'Root', children: [{ id: 11, name: 'Sub' }] }] }],
    })

    await completeStep1ToReady()
    await screen.findByText(/artwork details/i)

    const publishAs = screen.getByRole('combobox', { name: /publishing identity/i })
    expect(screen.getByRole('option', { name: /personal profile/i })).not.toBeNull()
    expect(screen.getByRole('option', { name: /warp collective/i })).not.toBeNull()

    await act(async () => {
      await userEvent.selectOptions(publishAs, 'warp-collective')
    })

    expect(await screen.findByRole('combobox', { name: /primary author/i })).not.toBeNull()
    expect(screen.getByText(/contributors/i)).not.toBeNull()
  })

  it('keeps mobile sticky action bar visible class', async () => {
    installAxiosStubs()
    await renderWizard({ initialDraftId: 306 })

    const bar = screen.getByTestId('wizard-action-bar')
    expect((bar.className || '').includes('sticky')).toBe(true)
    expect((bar.className || '').includes('bottom-0')).toBe(true)
  })

  it('shows mapped duplicate hash toast when finish returns duplicate_hash', async () => {
    installAxiosStubs({
      finishError: {
        response: {
          status: 409,
          data: {
            reason: 'duplicate_hash',
            message: 'Duplicate upload is not allowed. This file already exists.',
          },
        },
      },
    })

    await renderWizard({ initialDraftId: 310 })
    await uploadPrimary(new File(['img'], 'duplicate.png', { type: 'image/png' }))

    await act(async () => {
      await userEvent.click(await screen.findByRole('button', { name: /start upload/i }))
    })

    const toast = await screen.findByRole('alert')
    expect(toast.textContent).toMatch(/already exists in skinbase/i)
  })

  it('locks step 1 file input after upload and unlocks after reset', async () => {
    installAxiosStubs({ statusValue: 'ready' })
    await renderWizard({ initialDraftId: 309 })

    await completeStep1ToReady()

    const stepper = screen.getByRole('navigation', { name: /upload steps/i })
    await act(async () => {
      await userEvent.click(within(stepper).getByRole('button', { name: /upload/i }))
    })

    await waitFor(() => {
      const dropzoneButton = screen.getByTestId('upload-dropzone')
      expect(dropzoneButton.getAttribute('aria-disabled')).toBe('true')
    })
    expect(screen.getByText(/file is locked after upload starts\. reset to change the file\./i)).not.toBeNull()

    await act(async () => {
      await userEvent.click(screen.getByRole('button', { name: /reset upload/i }))
    })

    await waitFor(() => {
      const unlockedDropzone = screen.getByTestId('upload-dropzone')
      expect(unlockedDropzone.getAttribute('aria-disabled')).toBe('false')
    })
  })
})
