import { describe, it, expect, beforeEach } from 'vitest'
import { http } from 'msw'
import { server } from './msw/server'
import { jsonResponse, paginate } from './msw/helpers'
import { renderWithProviders, screen, fireEvent, waitFor } from './helpers/render'
import { seedBearer, clearBearer } from './helpers/auth'
import { FlightProvider } from '../src/components'
import { DatasetPage } from '../src/features/dataset'
import { AnnotationStatus, AnnotationSource } from '../src/types'
import type { DatasetItem } from '../src/types'

// AZ-464 — Bulk-validate URL + body + UI sync within 2 s.
//
// AC-1 (FT-P-20 URL): outbound POST URL is `/api/annotations/dataset/bulk-status`.
// AC-2 (FT-P-20 body): outbound body carries the media-id set + the target
//                      status. Spec contract is `{ids, targetStatus: 30}`
//                      (post-AC-04 enum scheme); production today emits
//                      `{annotationIds, status: 2}`. Two `it.fails()` tests
//                      pin the documented drifts (field names + status value)
//                      and a control pins the current behavior.
// AC-3 (FT-P-21 + NFT-PERF-07): after a 200 from the POST, every selected
//                      row's DOM badge reads `Validated` within 2 s. The
//                      production handler awaits the POST response then calls
//                      fetchItems() — the second GET returns updated items.

const seedItems: DatasetItem[] = [
  {
    annotationId: 'ann-az464-1',
    imageName: 'az464-1.jpg',
    thumbnailPath: '/thumbs/az464-1.jpg',
    status: AnnotationStatus.Created,
    createdDate: '2026-05-11T10:00:00Z',
    createdEmail: 'op_alice@test.local',
    flightName: 'Flight A',
    source: AnnotationSource.Manual,
    isSeed: false,
    isSplit: false,
  },
  {
    annotationId: 'ann-az464-2',
    imageName: 'az464-2.jpg',
    thumbnailPath: '/thumbs/az464-2.jpg',
    status: AnnotationStatus.Created,
    createdDate: '2026-05-11T10:01:00Z',
    createdEmail: 'op_alice@test.local',
    flightName: 'Flight A',
    source: AnnotationSource.Manual,
    isSeed: false,
    isSplit: false,
  },
  {
    annotationId: 'ann-az464-3',
    imageName: 'az464-3.jpg',
    thumbnailPath: '/thumbs/az464-3.jpg',
    status: AnnotationStatus.Created,
    createdDate: '2026-05-11T10:02:00Z',
    createdEmail: 'op_alice@test.local',
    flightName: 'Flight A',
    source: AnnotationSource.Manual,
    isSeed: false,
    isSplit: false,
  },
]

interface CapturedBulk {
  url: string
  pathname: string
  body: Record<string, unknown>
}

interface SyncRig {
  posts: CapturedBulk[]
  validatedAfterPost: { current: boolean }
}

function rigDatasetAndBulk(): SyncRig {
  const posts: CapturedBulk[] = []
  const validatedAfterPost = { current: false }

  server.use(
    http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
    http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
    http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
    http.put('/api/annotations/settings/user', () => new Response(null, { status: 200 })),
    http.get('/api/annotations/classes', () => jsonResponse([])),
    // Dataset list — returns the seeded items, paginated. After the bulk POST
    // fires, this handler flips its `status` field to Validated for the
    // entire seed so the second GET delivers the updated payload.
    http.get('/api/annotations/dataset', () => {
      const items = seedItems.map((it) =>
        validatedAfterPost.current
          ? { ...it, status: AnnotationStatus.Validated }
          : { ...it },
      )
      return jsonResponse(paginate(items, 1, items.length))
    }),
    http.post('/api/annotations/dataset/bulk-status', async ({ request }) => {
      const body = (await request.json()) as Record<string, unknown>
      const url = new URL(request.url)
      posts.push({
        url: request.url,
        pathname: url.pathname,
        body,
      })
      // Flip the GET handler so the next fetchItems() returns updated rows.
      validatedAfterPost.current = true
      return jsonResponse({ updated: 3, status: 30 })
    }),
  )

  return { posts, validatedAfterPost }
}

async function selectItemsWithCtrlClick(annotationIds: string[]): Promise<void> {
  // The DatasetPage doesn't expose row test-ids; row identity lives in
  // imageName + annotationId. Locate each row by its image name.
  for (const id of annotationIds) {
    const item = seedItems.find((s) => s.annotationId === id)!
    const cell = await screen.findByText(item.imageName)
    // Walk to the parent row that owns the onClick handler. The row is the
    // outer `<div>` rendered for each item; its className contains
    // `cursor-pointer`. Use `closest(...)` against a stable structural
    // selector to be resilient to copy edits.
    const row = cell.closest('div.cursor-pointer')
    expect(row).toBeTruthy()
    fireEvent.click(row!, { ctrlKey: true })
  }
}

describe('AZ-464 — bulk-validate URL + body + UI sync', () => {
  beforeEach(() => {
    seedBearer()
  })

  describe('AC-1 (FT-P-20) — URL canary', () => {
    it('clicking Validate fires exactly one POST against `/api/annotations/dataset/bulk-status`', async () => {
      // Arrange
      const { posts } = rigDatasetAndBulk()
      renderWithProviders(
        <FlightProvider>
          <DatasetPage />
        </FlightProvider>,
      )

      // Wait for items to render.
      await screen.findByText(seedItems[0].imageName)

      // Act — Ctrl+click the 3 seed items, then click Validate.
      await selectItemsWithCtrlClick(seedItems.map((it) => it.annotationId))
      const validateBtn = await screen.findByRole('button', { name: /Validate \(\d+\)/i })
      fireEvent.click(validateBtn)

      // Assert — exactly one POST observed; URL matches contract.
      await waitFor(() => expect(posts).toHaveLength(1), { timeout: 3000 })
      expect(posts[0].pathname).toBe('/api/annotations/dataset/bulk-status')

      clearBearer()
    })
  })

  describe('AC-2 (FT-P-20) — body shape', () => {
    it.fails(
      'body carries `{ids: <N>, targetStatus: 30}` per contract',
      async () => {
        // Production today sends `{annotationIds: <N>, status: 2}` — both
        // field names AND the status value differ from the contract. The
        // assertion below fails on either drift; flips green when production
        // aligns with the AC-04 wire enum scheme.
        const { posts } = rigDatasetAndBulk()
        renderWithProviders(
          <FlightProvider>
            <DatasetPage />
          </FlightProvider>,
        )
        await screen.findByText(seedItems[0].imageName)
        await selectItemsWithCtrlClick(seedItems.map((it) => it.annotationId))
        const validateBtn = await screen.findByRole('button', { name: /Validate \(\d+\)/i })
        fireEvent.click(validateBtn)
        await waitFor(() => expect(posts).toHaveLength(1), { timeout: 3000 })

        const body = posts[0].body
        expect(body).toHaveProperty('ids')
        expect(Array.isArray(body.ids)).toBe(true)
        expect((body.ids as unknown[])).toHaveLength(seedItems.length)
        expect(body).toHaveProperty('targetStatus', 30)

        clearBearer()
      },
    )

    it('control: production sends `{annotationIds, status: AnnotationStatus.Validated}` (current drift shape)', async () => {
      // Pin the CURRENT shape so a regression that drops `annotationIds` or
      // changes `status` to a non-enum value is caught even before AC-2 flips
      // green. When AC-04 lands the wire enum scheme, this control needs to
      // be adjusted alongside production.
      const { posts } = rigDatasetAndBulk()
      renderWithProviders(
        <FlightProvider>
          <DatasetPage />
        </FlightProvider>,
      )
      await screen.findByText(seedItems[0].imageName)
      await selectItemsWithCtrlClick(seedItems.map((it) => it.annotationId))
      const validateBtn = await screen.findByRole('button', { name: /Validate \(\d+\)/i })
      fireEvent.click(validateBtn)
      await waitFor(() => expect(posts).toHaveLength(1), { timeout: 3000 })

      const body = posts[0].body
      expect(body).toHaveProperty('annotationIds')
      expect(Array.isArray(body.annotationIds)).toBe(true)
      expect((body.annotationIds as unknown[])).toHaveLength(seedItems.length)
      expect(body).toHaveProperty('status', AnnotationStatus.Validated)

      clearBearer()
    })
  })

  describe('AC-3 (FT-P-21 + NFT-PERF-07) — UI sync within 2 s', () => {
    it('every selected row badge reads `Validated` ≤ 2 000 ms after the POST resolves', async () => {
      // Arrange
      const { posts } = rigDatasetAndBulk()
      renderWithProviders(
        <FlightProvider>
          <DatasetPage />
        </FlightProvider>,
      )
      await screen.findByText(seedItems[0].imageName)
      await selectItemsWithCtrlClick(seedItems.map((it) => it.annotationId))
      const validateBtn = await screen.findByRole('button', { name: /Validate \(\d+\)/i })

      // Act — record wall-clock at click time so the perf budget is observed.
      const t0 = Date.now()
      fireEvent.click(validateBtn)

      // Assert — POST observed, then all rows show the Validated badge.
      await waitFor(() => expect(posts).toHaveLength(1), { timeout: 3000 })

      // The Validated badge text comes from i18n key `dataset.status.validated`,
      // resolving to 'Validated' in the en bundle. Every seedItem row has
      // exactly one badge `<span>` inside the row card.
      await waitFor(
        () => {
          const validatedBadges = screen.getAllByText('Validated')
          // The status-filter button bar also contains a 'Validated' button —
          // filter to the badge spans (size class `px-1 rounded` is unique to
          // the badge in DatasetPage's row template).
          const rowBadges = validatedBadges.filter((el) =>
            (el.className ?? '').includes('px-1 rounded'),
          )
          expect(rowBadges).toHaveLength(seedItems.length)
        },
        { timeout: 2000 },
      )
      const elapsed = Date.now() - t0
      expect(elapsed).toBeLessThanOrEqual(2000)

      clearBearer()
    })
  })
})
