import { describe, it, expect, beforeEach } from 'vitest'
import { http } from 'msw'
import { server } from './msw/server'
import { jsonResponse } from './msw/helpers'
import { renderWithProviders, screen, waitFor, userEvent } from './helpers/render'
import { seedBearer, clearBearer } from './helpers/auth'
import { SettingsPage } from '../src/features/settings'

// AZ-475 — Numeric form input — empty / non-numeric rejection
//
// AC-1 (FT-N-11): clearing a numeric field MUST surface a validation error
//                 and prevent the PUT from firing. Silent zero is a regression.
// AC-2 (FT-N-12): typing a non-numeric value MUST surface a validation error
//                 and prevent the PUT from firing.
//
// Production drift (`src/features/settings/SettingsPage.tsx:38-48, 59-60`):
//   1. `<label>` carries no `htmlFor` — labels are not programmatically
//      associated with their inputs (a separate a11y drift surfaced by this
//      test's setup; the test works around it via DOM traversal). Phase B
//      task should add `id`/`htmlFor` so `getByLabelText` works directly
//      and screen readers can navigate the form.
//   2. `parseInt(v) || 0` and `parseFloat(v) || 0` silently coerce empty
//      input to 0 with no validation, then the save handler PUTs the
//      zeroed payload. FT-N-11 / FT-N-12 are recorded as `it.fails()`
//      until production lands a `useNumericField` validator (or equivalent)
//      that blocks save on invalid input.

function inputForLabel(labelText: RegExp | string): HTMLInputElement {
  // SettingsPage's `<label>` is a sibling of the `<input>` inside a wrapper
  // `<div>` (no `htmlFor`). Find the label, walk to its parent, then to the
  // input. Once production lands `htmlFor` (drift #1 above), tests can use
  // `screen.findByLabelText` directly.
  const label = screen.getByText(labelText, { selector: 'label' })
  const wrapper = label.parentElement
  if (!wrapper) throw new Error(`label "${String(labelText)}" has no parent`)
  const input = wrapper.querySelector('input')
  if (!input) throw new Error(`no input next to label "${String(labelText)}"`)
  return input as HTMLInputElement
}

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

function captureSettingsPut(): { puts: CapturedPut[] } {
  const puts: CapturedPut[] = []
  server.use(
    http.put('/api/annotations/settings/system', async ({ request }) => {
      puts.push({
        url: new URL(request.url).pathname,
        body: (await request.json()) as Record<string, unknown>,
      })
      return jsonResponse({ ok: true })
    }),
    // Settings page bootstraps three GETs.
    http.get('/api/annotations/settings/system', () =>
      jsonResponse({
        id: 'sys-az475',
        name: 'AZ-475 system',
        militaryUnit: null,
        defaultCameraWidth: 1920,
        defaultCameraFoV: 60,
      }),
    ),
    http.get('/api/annotations/settings/directories', () =>
      jsonResponse({
        id: 'dirs-az475',
        videosDir: '/srv/v',
        imagesDir: '/srv/i',
        labelsDir: '/srv/l',
        resultsDir: '/srv/r',
        thumbnailsDir: '/srv/t',
        gpsSatDir: '/srv/gs',
        gpsRouteDir: '/srv/gr',
      }),
    ),
    http.get('/api/flights/aircrafts', () => jsonResponse([])),
    http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
  )
  return { puts }
}

describe('AZ-475 — numeric form input rejection', () => {
  beforeEach(() => {
    seedBearer()
  })

  describe('AC-1 (FT-N-11) — empty numeric input', () => {
    it.fails(
      'shows a validation error and DOES NOT issue the PUT when the field is cleared',
      async () => {
        // Arrange
        const { puts } = captureSettingsPut()
        renderWithProviders(<SettingsPage />)
        await screen.findByText(/Default Camera Width/i)
        const widthInput = inputForLabel(/Default Camera Width/i)
        expect(widthInput).toBeInTheDocument()

        // Act
        await userEvent.clear(widthInput)
        // Find the matching Save button (first Save in tenant config block).
        const saveButtons = await screen.findAllByRole('button', { name: /Save/i })
        await userEvent.click(saveButtons[0])

        // Assert — validation message present, no PUT issued.
        // Drift today: SettingsPage uses `parseInt(v) || 0` (silent zero) AND
        // issues the PUT regardless. Both halves of this assertion fail.
        const error = await screen.findByText(/required|invalid|must be a number/i, undefined, {
          timeout: 1000,
        })
        expect(error).toBeInTheDocument()
        await new Promise(r => setTimeout(r, 50))
        expect(puts).toHaveLength(0)
        clearBearer()
      },
    )

    it('control: production today silently coerces empty input to 0 and PUTs', async () => {
      // Pin current behavior so a regression that, e.g., starts crashing on
      // empty input is caught even before AC-1 is fixed. When AC-1 lands,
      // this control flips red and is removed.
      const { puts } = captureSettingsPut()
      renderWithProviders(<SettingsPage />)
      await screen.findByText(/Default Camera Width/i)
      const widthInput = inputForLabel(/Default Camera Width/i)

      await userEvent.clear(widthInput)
      const saveButtons = await screen.findAllByRole('button', { name: /Save/i })
      await userEvent.click(saveButtons[0])

      await waitFor(() => expect(puts).toHaveLength(1), { timeout: 1000 })
      expect(puts[0].body).toMatchObject({ defaultCameraWidth: 0 })
      clearBearer()
    })
  })

  describe('AC-2 (FT-N-12) — non-numeric input', () => {
    it.fails(
      'shows a validation error and DOES NOT issue the PUT when input is non-numeric',
      async () => {
        // Arrange
        const { puts } = captureSettingsPut()
        renderWithProviders(<SettingsPage />)
        await screen.findByText(/Default Camera Width/i)
        const widthInput = inputForLabel(/Default Camera Width/i)

        // Act — `<input type="number">` ignores non-numeric typed chars in browsers,
        // BUT user-event still fires onChange events. To force a non-numeric value
        // through the React state we set the value directly via fireEvent on
        // input. (`userEvent.type` would no-op on a number input for "abc".)
        await userEvent.clear(widthInput)
        widthInput.value = 'abc'
        widthInput.dispatchEvent(new Event('input', { bubbles: true }))
        widthInput.dispatchEvent(new Event('change', { bubbles: true }))

        const saveButtons = await screen.findAllByRole('button', { name: /Save/i })
        await userEvent.click(saveButtons[0])

        // Assert — validation error visible; no PUT.
        const error = await screen.findByText(/invalid|must be a number/i, undefined, {
          timeout: 1000,
        })
        expect(error).toBeInTheDocument()
        await new Promise(r => setTimeout(r, 50))
        expect(puts).toHaveLength(0)
        clearBearer()
      },
    )
  })
})
