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, waitFor, userEvent } from './helpers/render'
import { seedBearer, clearBearer } from './helpers/auth'
import { FlightProvider } from '../src/components'
import { AnnotationsPage } from '../src/features/annotations'
import { AnnotationSource, AnnotationStatus, MediaType, MediaStatus, Affiliation, CombatReadiness } from '../src/types'
import type { Media, AnnotationListItem, Detection } from '../src/types'

// AZ-460 — Annotation save URL + payload contract
//
// AC-1 (FT-P-07): outbound URL is the doubly-prefixed canary
//                 `/api/annotations/annotations` (gateway prefix + service base).
// AC-2 (FT-P-08): outbound body contains all required fields:
//                 Source, WaypointId, videoTime, mediaId, detections, status.
// AC-3:           the required-fields check runs for at least three save entry
//                 points (AI suggestion accept, manual draw, bulk-edit save).
//                 Production today only exposes ONE save path (`<AnnotationsPage>`'s
//                 Save button); AI-suggestion-accept and bulk-edit-save are not yet
//                 wired in production. Those two scenarios are recorded as
//                 `it.skip` QUARANTINE entries until Phase B lands them.

const seedDetection: Detection = {
  id: 'det-existing',
  classNum: 0,
  label: 'class-0',
  confidence: 0.92,
  affiliation: Affiliation.Hostile,
  combatReadiness: CombatReadiness.Ready,
  centerX: 0.4,
  centerY: 0.5,
  width: 0.1,
  height: 0.15,
}

const seedMediaItem: Media = {
  id: 'media-az460',
  name: 'az460.jpg',
  path: '/media/az460.jpg',
  mediaType: MediaType.Image,
  mediaStatus: MediaStatus.New,
  duration: null,
  annotationCount: 1,
  waypointId: 'wp-az460',
  userId: 'user-az460',
}

const seedAnn: AnnotationListItem = {
  id: 'ann-az460',
  mediaId: seedMediaItem.id,
  time: null,
  createdDate: '2026-05-11T00:00:00Z',
  userId: seedMediaItem.userId,
  source: AnnotationSource.Manual,
  status: AnnotationStatus.Created,
  isSplit: false,
  splitTile: null,
  detections: [seedDetection],
}

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

function captureSavePost(): { saves: CapturedSave[] } {
  // Arrange — capture every POST to the doubly-prefixed annotation save endpoint.
  const saves: CapturedSave[] = []
  server.use(
    http.post('/api/annotations/annotations', async ({ request }) => {
      saves.push({
        url: new URL(request.url).pathname,
        body: (await request.json()) as Record<string, unknown>,
      })
      return jsonResponse(
        { id: 'ann-saved', createdDate: new Date().toISOString() },
        { status: 201 },
      )
    }),
    // Background bootstrap — FlightContext + DetectionClasses + initial AnnotationsPage mount.
    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 })),
    // MediaList fetch + per-selection annotation list reload.
    http.get('/api/annotations/media', ({ request }) => {
      const url = new URL(request.url)
      const page = Number(url.searchParams.get('page') ?? '1')
      const pageSize = Number(url.searchParams.get('pageSize') ?? '1000')
      return jsonResponse(paginate([seedMediaItem], page, pageSize))
    }),
    http.get('/api/annotations/annotations', ({ request }) => {
      const url = new URL(request.url)
      const mediaId = url.searchParams.get('mediaId')
      const items = mediaId === seedMediaItem.id ? [seedAnn] : []
      const page = Number(url.searchParams.get('page') ?? '1')
      const pageSize = Number(url.searchParams.get('pageSize') ?? '1000')
      return jsonResponse(paginate(items, page, pageSize))
    }),
    http.get('/api/annotations/classes', () => jsonResponse([])),
    http.get('/api/annotations/dataset/info', () => jsonResponse({ totalCount: 1, statusCounts: {} })),
    http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
  )
  return { saves }
}

async function selectMediaAndAnnotation(): Promise<void> {
  // Wait for media to load and click it. (`findByText` returns the inner
  // <span>; click bubbles to the row's onClick handler.)
  const mediaItem = await screen.findByText('az460.jpg')
  await userEvent.click(mediaItem)
  // After click, MediaList GETs `/api/annotations/annotations?mediaId=...` and
  // calls `onAnnotationsLoaded`. AnnotationsSidebar then renders an annotation
  // row showing `'—'` (no time) and the first detection's label (`class-0`).
  // Clicking that label fires `handleAnnotationSelect`, which seeds detections
  // and enables the Save button. Wait up to 3 s for the row to appear.
  const detectionLabel = await screen.findByText('class-0', undefined, { timeout: 3000 })
  await userEvent.click(detectionLabel)
}

describe('AZ-460 — annotation save URL + payload contract', () => {
  beforeEach(() => {
    seedBearer()
  })

  describe('AC-1 (FT-P-07) — URL canary', () => {
    it('issues the save POST against the doubly-prefixed `/api/annotations/annotations` path', async () => {
      // Arrange
      const { saves } = captureSavePost()
      renderWithProviders(
        <FlightProvider>
          <AnnotationsPage />
        </FlightProvider>,
      )

      // Act
      await selectMediaAndAnnotation()
      // Wait for the side-effect: clicking annotation populates detections.
      await waitFor(() => {
        const saveBtn = screen.getByRole('button', { name: /^Save$/i })
        expect(saveBtn).not.toBeDisabled()
      }, { timeout: 3000 })
      const saveBtn = screen.getByRole('button', { name: /^Save$/i })
      await userEvent.click(saveBtn)

      // Assert
      await waitFor(() => expect(saves).toHaveLength(1), { timeout: 2000 })
      expect(saves[0].url).toBe('/api/annotations/annotations')
      // Negative canary — single-prefix would silently match if the URL
      // regressed; the equality above is the gating assertion.
      expect(saves[0].url).not.toBe('/api/annotations')
      clearBearer()
    })
  })

  describe('AC-2 (FT-P-08) — required-fields presence', () => {
    it.fails(
      'includes ALL of {Source, WaypointId, videoTime, mediaId, detections, status} in the save body',
      async () => {
        // Arrange — production today sends only {mediaId, time, detections}
        // (see `src/features/annotations/AnnotationsPage.tsx:32-44`). The other
        // four fields are missing. This drift is documented as `it.fails()`
        // until Phase B lifts the body shape to match the wire contract.
        const { saves } = captureSavePost()
        renderWithProviders(
          <FlightProvider>
            <AnnotationsPage />
          </FlightProvider>,
        )

        // Act
        await selectMediaAndAnnotation()
        await waitFor(() => {
          const saveBtn = screen.getByRole('button', { name: /^Save$/i })
          expect(saveBtn).not.toBeDisabled()
        }, { timeout: 3000 })
        await userEvent.click(screen.getByRole('button', { name: /^Save$/i }))

        // Assert — every required field present.
        await waitFor(() => expect(saves).toHaveLength(1))
        const body = saves[0].body
        expect(body).toHaveProperty('mediaId', seedMediaItem.id)
        expect(body).toHaveProperty('detections')
        expect(Array.isArray(body.detections)).toBe(true)
        // The four drift fields — the assertion `it.fails()` flips green when these land.
        expect(body).toHaveProperty('Source')
        expect(['AI', 'Manual']).toContain(body.Source)
        expect(body).toHaveProperty('WaypointId')
        expect(body).toHaveProperty('videoTime')
        expect(body).toHaveProperty('status')
        clearBearer()
      },
    )

    it('asserts the partial body shape that production currently emits (control)', async () => {
      // This control test pins the CURRENT (drift) shape so a regression that
      // drops `mediaId` or `detections` is caught even before AC-2 flips green.
      // Once production lands the full contract, this test stays green; the
      // `it.fails()` above starts passing and the migration is observable.
      const { saves } = captureSavePost()
      renderWithProviders(
        <FlightProvider>
          <AnnotationsPage />
        </FlightProvider>,
      )

      await selectMediaAndAnnotation()
      await waitFor(() => {
        const saveBtn = screen.getByRole('button', { name: /^Save$/i })
        expect(saveBtn).not.toBeDisabled()
      }, { timeout: 3000 })
      await userEvent.click(screen.getByRole('button', { name: /^Save$/i }))

      await waitFor(() => expect(saves).toHaveLength(1))
      const body = saves[0].body
      expect(body.mediaId).toBe(seedMediaItem.id)
      expect(Array.isArray(body.detections)).toBe(true)
      expect((body.detections as unknown[]).length).toBeGreaterThan(0)
      clearBearer()
    })
  })

  describe('AC-3 — multiple save entry points', () => {
    it('exercises the manual-draw / select-existing save entry point', async () => {
      // The covered case from AC-1 / AC-2 above. Recorded here as a separate
      // assertion so the AC-3 entry-point list is explicit in the report.
      const { saves } = captureSavePost()
      renderWithProviders(
        <FlightProvider>
          <AnnotationsPage />
        </FlightProvider>,
      )
      await selectMediaAndAnnotation()
      await waitFor(() => {
        expect(screen.getByRole('button', { name: /^Save$/i })).not.toBeDisabled()
      }, { timeout: 3000 })
      await userEvent.click(screen.getByRole('button', { name: /^Save$/i }))
      await waitFor(() => expect(saves.length).toBeGreaterThan(0))
      clearBearer()
    })

    it.skip(
      'QUARANTINE — AI-suggestion-accept save entry point not yet wired in production',
      async () => {
        // Production has no "accept AI suggestion" button that fires a save —
        // AI suggestions arrive via the detect/* services and are merged into
        // detections, but the user accepts via the same `Save` button as manual
        // draw. The intended distinct UX where accepting an AI suggestion
        // issues its own save (with `Source: 'AI'`) lands in Phase B.
        // When the path lands, flip this test to a real assertion that issues
        // the AI-flavored save and captures `Source === 'AI'`.
      },
    )

    it.skip(
      'QUARANTINE — bulk-edit save entry point not yet wired in production',
      async () => {
        // Production has no bulk-edit save path. Bulk operations exist via the
        // dataset bulk-status endpoint (`/api/annotations/dataset/bulk-status`)
        // but that does not issue an annotation save per item. When a true
        // bulk-edit save lands, this test issues it and asserts each save
        // body matches the AC-2 contract.
      },
    )
  })
})
