import { expect, test } from '@playwright/test'
import { startFixtureCollector, type FixtureCollector } from './fixture-collector'
import { ADAPTER_STATUSES, ALL_ITEMS, ALL_PANELS, RESOLVABLE_ITEM_ID } from './fixtures/collector-fixtures'
import {
  CI_PANEL_HISTORY_INCOMPLETE,
  CI_PANEL_QUEUE_INCOMPLETE,
  CI_PANEL_QUEUE_NULL,
} from './fixtures/ci-build-fixtures'

// The fixture collector binds one fixed port shared by the single Astro dev server
// (its URL is baked into apps/web/.env at server startup) — these tests must run in
// one worker, not fullyParallel's default per-test workers, or beforeAll races on it.
test.describe.configure({ mode: 'serial' })

let collector: FixtureCollector

function panelsWithCi(ciPanel: (typeof ALL_PANELS)[number]) {
  return ALL_PANELS.map((panel) => panel.id === 'ci' ? ciPanel : panel)
}

test.beforeAll(async () => {
  collector = await startFixtureCollector({ panels: ALL_PANELS, adapters: ADAPTER_STATUSES, items: ALL_ITEMS })
})

test.afterAll(async () => {
  await collector.close()
})

test('renders every mockup-v2 Overview widget group with fixture values, no console errors', async ({ page }) => {
  const errors: string[] = []
  page.on('console', (msg) => {
    if (msg.type() === 'error') errors.push(msg.text())
  })
  page.on('pageerror', (err) => errors.push(err.message))

  await page.goto('/')
  await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')

  // (a) scoreboard strip
  await expect(page.getByRole('heading', { level: 4, name: /^multideal/ })).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'zync.is' })).toBeVisible()
  await expect(page.getByText('/34')).toBeVisible()

  // (b) needs-you-now — top 4 of 5 fixture items, by severity then recency
  await expect(page.getByRole('heading', { level: 4, name: /^Needs you now/ })).toBeVisible()
  await expect(page.getByText('e2e failed on main')).toBeVisible()
  await expect(page.getByText('Train #4901 failed on infrastructure')).toBeVisible()
  await expect(page.getByText('Cancel obsolete train gate')).toBeVisible()
  await expect(page.getByText('dep-provisioning halted')).toHaveCount(0)
  await expect(page.getByText('informational note')).toHaveCount(0)

  // (c) limits
  await expect(page.getByText('claude · claude A', { exact: true })).toBeVisible()
  await expect(page.getByText('spend today')).toBeVisible()
  await expect(page.getByText('$11.40')).toBeVisible()

  // (d) forensics row
  await expect(page.getByText(/Run forensics — autonomy-p4/)).toBeVisible()
  await expect(page.getByText('wall clock')).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'Where the time went' })).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: /^Runs/ })).toBeVisible()

  // (e) bots / ci / plans / hosts & gates
  await expect(page.getByRole('heading', { level: 4, name: /^Bots \(Botmaster\)/ })).toBeVisible()
  await expect(page.getByText('dealbot')).toBeVisible()
  await expect(page.getByText('zyncbot')).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'CI & Runners' })).toBeVisible()
  await expect(page.getByText(/alexcodeplace\/multideal #4821/)).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'Plans' })).toBeVisible()
  await expect(page.getByText(/wave 2\/2/)).toBeVisible()
  await expect(page.getByText('HALT · failed')).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'Hosts & Gates' })).toBeVisible()
  await expect(page.getByText('PSI cpu some')).toBeVisible()
  await expect(page.getByText('4.1%')).toBeVisible()

  // stale handling: the prometheus adapter fixture is marked stale — its card shows StaleBadge
  await expect(page.getByRole('heading', { level: 4, name: 'Hosts & Gates' })).toContainText('stale')

  expect(errors).toEqual([])
})

test('renders correctly in light theme with no console errors', async ({ page }) => {
  const errors: string[] = []
  page.on('console', (msg) => {
    if (msg.type() === 'error') errors.push(msg.text())
  })
  page.on('pageerror', (err) => errors.push(err.message))

  await page.goto('/')
  await expect(page.locator('[role="radiogroup"][aria-label="Theme"]')).toHaveAttribute('data-hydrated', 'true')
  await page.getByRole('radio', { name: /Light/ }).click()
  await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')

  await expect(page.getByRole('heading', { level: 4, name: /^multideal/ })).toBeVisible()
  await expect(page.getByText('e2e failed on main')).toBeVisible()
  await expect(page.getByText(/Run forensics — autonomy-p4/)).toBeVisible()
  await expect(page.getByRole('heading', { level: 4, name: 'Hosts & Gates' })).toContainText('stale')

  expect(errors).toEqual([])
})

test('an item-resolved SSE delta removes the item from Needs you now', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByText('e2e failed on main')).toBeVisible()

  await collector.waitForClient()
  collector.emit({ type: 'item-resolved', id: RESOLVABLE_ITEM_ID })

  await expect(page.getByText('e2e failed on main')).toHaveCount(0)
})

test('renders queue completeness independently from run history', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByText('alexcodeplace/multideal queue')).toBeVisible()
  await expect(page.getByText('2 waiting')).toBeVisible()

  await collector.close()
  collector = await startFixtureCollector({
    panels: panelsWithCi(CI_PANEL_HISTORY_INCOMPLETE),
    adapters: ADAPTER_STATUSES,
    items: ALL_ITEMS,
  })
  await page.reload()
  await expect(page.getByText('alexcodeplace/multideal queue')).toBeVisible()
  await expect(page.getByText('2 waiting')).toBeVisible()
  const historyGapCard = page.getByRole('heading', { level: 4, name: 'CI & Runners' }).locator('..')
  await expect(historyGapCard).toContainText('run history incomplete')
  await expect(historyGapCard).not.toContainText('alexcodeplace/multideal #4821')

  for (const incompletePanel of [CI_PANEL_QUEUE_INCOMPLETE, CI_PANEL_QUEUE_NULL]) {
    await collector.close()
    collector = await startFixtureCollector({
      panels: panelsWithCi(incompletePanel),
      adapters: ADAPTER_STATUSES,
      items: ALL_ITEMS,
    })
    await page.reload()
    const ciCard = page.getByRole('heading', { level: 4, name: 'CI & Runners' }).locator('..')
    await expect(ciCard).toContainText('queue incomplete')
    await expect(ciCard).not.toContainText('null waiting')
  }
})

test('keeps overview content inside mobile, tablet, laptop, and desktop viewports', async ({ page }) => {
  for (const viewport of [
    { width: 360, height: 800 },
    { width: 768, height: 1024 },
    { width: 1024, height: 768 },
    { width: 1440, height: 900 },
  ]) {
    await page.setViewportSize(viewport)
    await page.goto('/', { waitUntil: 'domcontentloaded' })
    await page.getByRole('heading', { level: 4, name: 'Hosts & Gates' }).waitFor()

    const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
    expect(overflow, `${viewport.width}px viewport has horizontal overflow`).toBeLessThanOrEqual(1)

    for (const name of [/^Needs you now/, 'Limits', 'CI & Runners', 'Plans', 'Hosts & Gates']) {
      const box = await page.getByRole('heading', { level: 4, name }).boundingBox()
      expect(box, `${String(name)} missing at ${viewport.width}px`).not.toBeNull()
      expect(box!.x, `${String(name)} starts outside ${viewport.width}px`).toBeGreaterThanOrEqual(0)
      expect(box!.x + box!.width, `${String(name)} exceeds ${viewport.width}px`).toBeLessThanOrEqual(viewport.width + 1)
    }
  }
})
