import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import ScanPage from '../src/pages/ScanPage.js'
import type { Candidate, ReleasePreview } from '../src/types.js'

vi.mock('../src/components/Scanner.js', () => ({
  default: ({ onDetect }: { onDetect: (code: string) => void }) => (
    <button type="button" onClick={() => onDetect('5021592210629')}>
      fake-scan
    </button>
  ),
}))

vi.mock('../src/api.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../src/api.js')>()
  return {
    ...actual,
    api: {
      ...actual.api,
      lookupBarcode: vi.fn(),
      getReleasePreview: vi.fn(),
      addToCollection: vi.fn(),
    },
  }
})

import { api, ApiError } from '../src/api.js'

const candidate: Candidate = {
  id: 1001,
  artist: 'The Cinematic Orchestra',
  title: 'Motion',
  year: 1999,
  formats: ['CD'],
  labels: ['Ninja Tune'],
  country: 'UK',
  catno: 'ZENCD012',
  thumbUrl: null,
}

const preview: ReleasePreview = {
  release: { ...candidate, genres: [], tracklist: [{ position: '1', title: 'Overture' }], coverUrl: null, barcodes: ['5021592210629'] },
  duplicate: false,
  ripMatch: 'not_ripped',
  matchCandidates: [],
}

// The api module is mocked directly, so mocks resolve with parsed bodies.
function jsonOk(body: unknown) {
  return Promise.resolve(body)
}

beforeEach(() => {
  vi.mocked(api.lookupBarcode).mockReset()
  vi.mocked(api.getReleasePreview).mockReset()
  vi.mocked(api.addToCollection).mockReset()
})

function renderScan() {
  return render(
    <MemoryRouter initialEntries={['/scan']}>
      <ScanPage />
    </MemoryRouter>
  )
}

describe('ScanPage flow', () => {
  it('scan → candidates → confirm → added', async () => {
    vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
    vi.mocked(api.getReleasePreview).mockResolvedValue(jsonOk(preview) as never)
    vi.mocked(api.addToCollection).mockResolvedValue(
      jsonOk({
        id: 1,
        discogsReleaseId: 1001,
        title: 'Motion',
        artist: 'The Cinematic Orchestra',
        year: 1999,
        formats: ['CD'],
        genres: [],
        labels: ['Ninja Tune'],
        tracklist: [],
        catno: 'ZENCD012',
        country: 'UK',
        artworkUrl: null,
        barcodes: ['5021592210629'],
        dateAdded: '2026-08-29',
        ripOverride: null,
        ripStatus: 'not_ripped',
      }) as never
    )

    renderScan()
    await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))

    await waitFor(() => expect(screen.getByRole('button', { name: /motion/i })).toBeTruthy())
    await userEvent.click(screen.getByRole('button', { name: /motion/i }))

    await waitFor(() => expect(screen.getByText('Not ripped yet')).toBeTruthy())
    expect(screen.getByText('Overture')).toBeTruthy()

    await userEvent.click(screen.getByRole('button', { name: /add to collection/i }))
    await waitFor(() => expect(screen.getByText(/added to collection/i)).toBeTruthy())
    expect(api.addToCollection).toHaveBeenCalledWith({
      releaseId: 1001,
      barcode: '5021592210629',
    })
  })

  it('shows not-found guidance with a manual-search escape hatch', async () => {
    vi.mocked(api.lookupBarcode).mockRejectedValue(new ApiError(404, 'not_found'))
    renderScan()
    await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
    await waitFor(() => expect(screen.getByText(/nothing found/i)).toBeTruthy())
    expect(screen.getByRole('link', { name: /search manually/i }).getAttribute('href')).toBe(
      '/add?q=5021592210629'
    )
  })

  it('sends the picked match candidate for ambiguous rip matches', async () => {
    vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
    vi.mocked(api.getReleasePreview).mockResolvedValue(
      jsonOk({
        ...preview,
        ripMatch: 'ambiguous',
        matchCandidates: [{ id: 77, title: 'Motion', artist: 'Somebody Else' }],
      }) as never
    )
    vi.mocked(api.addToCollection).mockResolvedValue(jsonOk({}) as never)

    renderScan()
    await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
    await userEvent.click(await screen.findByRole('button', { name: /motion/i }))
    await waitFor(() => expect(screen.getByRole('radio', { name: /somebody else/i })).toBeTruthy())
    await userEvent.click(screen.getByRole('radio', { name: /somebody else/i }))
    await userEvent.click(screen.getByRole('button', { name: /add to collection/i }))
    await waitFor(() => expect(api.addToCollection).toHaveBeenCalledWith({
      releaseId: 1001,
      barcode: '5021592210629',
      matchAlbumId: 77,
    }))
  })

  it('shows a duplicate warning from the preview', async () => {
    vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
    vi.mocked(api.getReleasePreview).mockResolvedValue(jsonOk({ ...preview, duplicate: true }) as never)
    renderScan()
    await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
    await userEvent.click(await screen.findByRole('button', { name: /motion/i }))
    await waitFor(() => expect(screen.getByText(/already in your collection/i)).toBeTruthy())
  })

  it('links to settings when no discogs token is configured', async () => {
    vi.mocked(api.lookupBarcode).mockRejectedValue(new ApiError(409, 'no_discogs_token'))
    renderScan()
    await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
    await waitFor(() => expect(screen.getByText(/discogs token/i)).toBeTruthy())
    expect(screen.getByRole('link', { name: /settings/i }).getAttribute('href')).toBe('/settings')
  })
})
