import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { http } from 'msw'
import { server } from './msw/server'
import { jsonResponse, paginate } from './msw/helpers'
import { renderWithProviders, screen, fireEvent, waitFor, act } from './helpers/render'
import { seedBearer, clearBearer } from './helpers/auth'
import { FlightProvider } from '../src/components'
import { AnnotationsPage } from '../src/features/annotations'

// AZ-470 — Panel-width debounced PUT + rehydration.
//
// AC-1 (FT-P-37 + NFT-PERF-08): multiple resize events within 1 s yield
//                               exactly ONE outbound PUT (debounce window).
// AC-2 (FT-P-37 body):           the PUT body carries the `panelWidths` key.
// AC-3 (FT-P-38):                after reload with `seed_user_settings.panelWidths`
//                               set, the rendered panel widths match the seed.
//
// Documented drift (entire task is a Phase-B-target group):
//   `useResizablePanel` today (`src/hooks/useResizablePanel.ts`) only
//   manages local state — no `useDebounce`-driven PUT on resize-end, no
//   rehydration from `/api/annotations/settings/user`. All three ACs are
//   `it.fails()`. They flip green when `useResizablePanel` is wired to
//   `<UserSettings>`'s save path.
//
// Each `it.fails()` is paired with a control that pins the CURRENT (no-PUT,
// no-rehydration) behavior so a regression that, e.g., starts emitting
// duplicate PUTs is visible even before the AC flips green.

const SEED_LEFT = 280
const SEED_RIGHT = 320

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

interface PanelRig {
  puts: CapturedPut[]
  divider: () => HTMLElement
}

function rigPanelEnv(opts?: { seedSettings?: boolean }): { puts: CapturedPut[] } {
  const puts: CapturedPut[] = []

  server.use(
    http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
    http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
    // The user settings GET — when seedSettings is true, return a payload
    // that includes both the legacy per-page width fields AND a `panelWidths`
    // object as defined by the FT-P-37/38 contract. Production today does
    // not consume either, but a future rehydration implementation could read
    // either shape; AC-3 asserts the rendered widths equal the seed values
    // regardless of which shape carries them.
    http.get('/api/annotations/settings/user', () => {
      if (opts?.seedSettings) {
        return jsonResponse({
          id: 'user-settings-az470',
          userId: 'user-az470',
          selectedFlightId: null,
          annotationsLeftPanelWidth: SEED_LEFT,
          annotationsRightPanelWidth: SEED_RIGHT,
          datasetLeftPanelWidth: null,
          datasetRightPanelWidth: null,
          panelWidths: {
            annotationsLeft: SEED_LEFT,
            annotationsRight: SEED_RIGHT,
          },
        })
      }
      return new Response(null, { status: 404 })
    }),
    http.put('/api/annotations/settings/user', async ({ request }) => {
      const body = (await request.json()) as Record<string, unknown>
      puts.push({
        url: request.url,
        pathname: new URL(request.url).pathname,
        body,
      })
      return jsonResponse({ id: 'user-settings-az470', ...body })
    }),
    http.get('/api/annotations/media', () => jsonResponse(paginate([], 1, 1000))),
    http.get('/api/annotations/annotations', () => jsonResponse(paginate([], 1, 1000))),
    http.get('/api/annotations/classes', () => jsonResponse([])),
  )
  return { puts }
}

function findDivider(): HTMLElement {
  // The divider is the `<div>` with `cursor-col-resize` — in <AnnotationsPage>
  // there are two: between left panel ↔ center, and center ↔ right panel.
  // We use the first one for AC-1 / AC-2 (the left divider).
  const dividers = document.querySelectorAll<HTMLElement>('div.cursor-col-resize')
  if (!dividers.length) throw new Error('No resizable divider found in DOM')
  return dividers[0]
}

function simulateDrag(divider: HTMLElement, dx: number): void {
  // Production's `useResizablePanel.onMouseDown` sets `dragging.current=true`
  // and snapshots `clientX`. The window-level `mousemove` handler updates
  // width, the window-level `mouseup` handler clears `dragging.current`.
  fireEvent.mouseDown(divider, { clientX: 100 })
  fireEvent.mouseMove(window, { clientX: 100 + dx })
  fireEvent.mouseUp(window, { clientX: 100 + dx })
}

describe('AZ-470 — panel-width debounced PUT + rehydration', () => {
  beforeEach(() => {
    seedBearer()
    vi.useFakeTimers({ shouldAdvanceTime: true })
  })

  afterEach(() => {
    vi.useRealTimers()
    clearBearer()
  })

  describe('AC-1 (FT-P-37 + NFT-PERF-08) — debounce window', () => {
    it.fails(
      'multiple resize events within 1 s yield exactly ONE outbound PUT (drift — production never PUTs)',
      async () => {
        // Production today emits ZERO PUTs during a resize because
        // `useResizablePanel` has no settings writer. The assertion below
        // expects exactly one PUT and therefore fails until Phase B lands the
        // writer. When the writer arrives, this test flips green automatically.
        const { puts } = rigPanelEnv()
        renderWithProviders(
          <FlightProvider>
            <AnnotationsPage />
          </FlightProvider>,
        )
        // Wait for the page to render and the divider to appear.
        await waitFor(() => expect(document.querySelector('div.cursor-col-resize')).toBeTruthy())

        // Act — three back-to-back drag-ends (200 ms apart) within the 1 s
        // debounce window.
        const divider = findDivider()
        await act(async () => {
          simulateDrag(divider, 30)
          vi.advanceTimersByTime(200)
          simulateDrag(divider, 50)
          vi.advanceTimersByTime(200)
          simulateDrag(divider, 70)
          // Push past the debounce ceiling so any debounced PUT has had a
          // chance to fire.
          vi.advanceTimersByTime(1100)
        })

        // Assert — exactly one PUT against the user-settings endpoint.
        await waitFor(() => expect(puts).toHaveLength(1), { timeout: 1000 })
        expect(puts[0].pathname).toBe('/api/annotations/settings/user')
      },
    )

    it('control: production emits ZERO PUTs during a resize today', async () => {
      // Pin the current (no-writer) behavior so a regression that, e.g.,
      // starts firing on every mousemove is visible immediately.
      const { puts } = rigPanelEnv()
      renderWithProviders(
        <FlightProvider>
          <AnnotationsPage />
        </FlightProvider>,
      )
      await waitFor(() => expect(document.querySelector('div.cursor-col-resize')).toBeTruthy())
      const divider = findDivider()
      await act(async () => {
        simulateDrag(divider, 50)
        vi.advanceTimersByTime(2000)
      })
      // No writer wired in production → zero PUTs is the pinned drift.
      expect(puts).toHaveLength(0)
    })
  })

  describe('AC-2 (FT-P-37) — PUT body carries `panelWidths` field', () => {
    it.fails(
      'the captured PUT body carries the `panelWidths` field per contract',
      async () => {
        // Same drift as AC-1: production never PUTs, so `puts[0].body` does
        // not exist and the property assertion below throws. The test flips
        // green when (a) production starts PUTting AND (b) the body contains
        // `panelWidths`.
        const { puts } = rigPanelEnv()
        renderWithProviders(
          <FlightProvider>
            <AnnotationsPage />
          </FlightProvider>,
        )
        await waitFor(() => expect(document.querySelector('div.cursor-col-resize')).toBeTruthy())
        const divider = findDivider()
        await act(async () => {
          simulateDrag(divider, 40)
          vi.advanceTimersByTime(1100)
        })
        await waitFor(() => expect(puts).toHaveLength(1), { timeout: 1000 })
        expect(puts[0].body).toHaveProperty('panelWidths')
      },
    )
  })

  describe('AC-3 (FT-P-38) — rehydration on reload', () => {
    it.fails(
      'after boot with a seeded `UserSettings.panelWidths`, the rendered widths match the seed',
      async () => {
        // Production's `<AnnotationsPage>` calls `useResizablePanel(250, ...)`
        // and `useResizablePanel(200, ...)` — the constructor args are the
        // ONLY width seed. There is no `useEffect` that reads
        // `/api/annotations/settings/user` and calls `setWidth(seed)`. With
        // the seed at 280 / 320, the rendered widths therefore stay 250 / 200
        // until Phase B wires the rehydration.
        rigPanelEnv({ seedSettings: true })
        renderWithProviders(
          <FlightProvider>
            <AnnotationsPage />
          </FlightProvider>,
        )

        // Wait for the page to settle (auth refresh + flights bootstrap).
        await waitFor(() => expect(document.querySelector('div.cursor-col-resize')).toBeTruthy())

        // Read the live `style.width` of each panel container. The two
        // outer panel `<div>`s sit on either side of the dividers; we
        // identify them by their distinctive `flex flex-col shrink-0`
        // class chain.
        const panels = document.querySelectorAll<HTMLElement>('div.bg-az-panel.shrink-0')
        expect(panels.length).toBeGreaterThanOrEqual(2)

        const [leftPanel, rightPanel] = [panels[0], panels[panels.length - 1]]
        // Spec: widths equal seed within ±1 px.
        const leftWidth = parseFloat(leftPanel.style.width)
        const rightWidth = parseFloat(rightPanel.style.width)
        expect(Math.abs(leftWidth - SEED_LEFT)).toBeLessThanOrEqual(1)
        expect(Math.abs(rightWidth - SEED_RIGHT)).toBeLessThanOrEqual(1)
      },
    )

    it('control: production renders panels at constructor-arg defaults (250 / 200) ignoring seeded settings', async () => {
      rigPanelEnv({ seedSettings: true })
      renderWithProviders(
        <FlightProvider>
          <AnnotationsPage />
        </FlightProvider>,
      )
      await waitFor(() => expect(document.querySelector('div.cursor-col-resize')).toBeTruthy())
      const panels = document.querySelectorAll<HTMLElement>('div.bg-az-panel.shrink-0')
      const [leftPanel, rightPanel] = [panels[0], panels[panels.length - 1]]
      // Constructor defaults from `<AnnotationsPage>`: 250 px (left), 200 px (right).
      expect(parseFloat(leftPanel.style.width)).toBe(250)
      expect(parseFloat(rightPanel.style.width)).toBe(200)
    })
  })
})
