import { test, expect } from '@playwright/test'

// AZ-473 — e2e companion for FT-P-50 (yoloId on the wire).
//
// Task spec marks FT-P-48 / FT-P-49 fast-only. Only FT-P-50 — the wire
// offset arithmetic `classNum == classId + photoModeOffset` — is `fast +
// e2e`, because the contract is observable on the network and the bug
// shape (wrong offset on save) is invisible to fast unit tests once the
// SPA's PhotoModeContext is exercised through the real DetectionClasses
// fetch + AnnotationsPage save.
//
// The companion runs the wire assertion for ALL three modes (P=0, 20, 40)
// against the suite stack so a regression in any mode-offset path lights
// up immediately.

const ALICE_EMAIL = 'op_alice@test.local'
const ALICE_PASSWORD = 'TestPassword!23'

async function login(page: import('@playwright/test').Page): Promise<void> {
  await page.goto('/login')
  await page.getByLabel(/email/i).fill(ALICE_EMAIL)
  await page.getByLabel(/password/i).fill(ALICE_PASSWORD)
  await Promise.all([
    page.waitForResponse(
      (r) => r.url().includes('/api/admin/auth/login') && r.request().method() === 'POST',
    ),
    page.getByRole('button', { name: /sign in/i }).click(),
  ])
}

async function selectPhotoMode(
  page: import('@playwright/test').Page,
  modeOffset: number,
): Promise<void> {
  // PhotoMode UI surfaces the three offsets as toggle buttons (mode 0,
  // 20, 40). The button label uses the offset directly; if the suite seed
  // localizes them, fall back to a position-based selector.
  const byLabel = page.getByRole('button', { name: new RegExp(`mode\\s*${modeOffset}`, 'i') })
  if (await byLabel.first().isVisible({ timeout: 2000 }).catch(() => false)) {
    await byLabel.first().click()
    return
  }
  // Fallback — the buttons render in a fixed order [0, 20, 40].
  const idx = modeOffset === 0 ? 0 : modeOffset === 20 ? 1 : 2
  await page.locator('[data-testid="photo-mode-button"]').nth(idx).click()
}

async function drawAndSave(
  page: import('@playwright/test').Page,
): Promise<void> {
  const canvas = page.locator('canvas').first()
  const box = await canvas.boundingBox()
  if (!box) throw new Error('canvas missing bounding box')

  // Draw a small bbox so the page has something to save.
  await page.mouse.move(box.x + box.width * 0.4, box.y + box.height * 0.4)
  await page.mouse.down()
  await page.mouse.move(box.x + box.width * 0.6, box.y + box.height * 0.6, { steps: 8 })
  await page.mouse.up()

  const saveBtn = page.getByRole('button', { name: /^Save$/i }).first()
  if (await saveBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
    await saveBtn.click().catch(() => null)
  }
}

test.describe('AZ-473 — yoloId on the wire (e2e companion)', () => {
  for (const modeOffset of [0, 20, 40]) {
    test(`FT-P-50 — mode ${modeOffset}: classNum == classId + ${modeOffset}`, async ({
      page,
      browserName,
    }) => {
      test.skip(
        browserName !== 'chromium',
        'Pointer-event canvas drawing reliable on Chromium only; the wire contract is the same on every browser',
      )
      test.setTimeout(30_000)

      const captured: Array<Record<string, unknown>> = []
      await page.route('**/api/annotations/annotations**', async (route) => {
        const req = route.request()
        if (req.method() === 'POST') {
          const body = req.postData()
          if (body) captured.push(JSON.parse(body) as Record<string, unknown>)
        }
        await route.continue()
      })

      await login(page)
      await page.goto('/annotations')

      const canvas = page.locator('canvas').first()
      if (!(await canvas.isVisible({ timeout: 5000 }).catch(() => false))) {
        test.skip(true, 'Suite seed has no media for annotation')
      }

      await selectPhotoMode(page, modeOffset).catch(() => null)

      // Pick the first valid class for this mode. The DetectionClasses panel
      // renders the filtered list; clicking the first item selects it.
      const firstClass = page.locator('[data-testid^="class-row"], button[data-testid*="class"]').first()
      if (await firstClass.isVisible({ timeout: 2000 }).catch(() => false)) {
        await firstClass.click().catch(() => null)
      }

      await drawAndSave(page)
      await page.waitForTimeout(750)

      expect(captured.length, 'expected at least one save POST').toBeGreaterThan(0)

      type Detection = { classNum?: number; classId?: number }
      const lastBody = captured[captured.length - 1]
      const detections = (lastBody.detections as Detection[] | undefined) ?? []
      expect(detections.length, 'last save must include the drawn detection').toBeGreaterThan(0)

      for (const d of detections) {
        // Wire contract per AC-3: classNum == classId + photoModeOffset.
        // Some service variants drop classId from the wire and only emit
        // classNum — in that case we assert classNum is in the [P, P+20)
        // window for this mode rather than failing on a missing field.
        if (typeof d.classId === 'number' && typeof d.classNum === 'number') {
          expect(d.classNum).toBe(d.classId + modeOffset)
        } else if (typeof d.classNum === 'number') {
          expect(d.classNum).toBeGreaterThanOrEqual(modeOffset)
          expect(d.classNum).toBeLessThan(modeOffset + 20)
        } else {
          throw new Error('Detection has neither classNum nor classId — wire contract broken')
        }
      }
    })
  }
})
