import { expect, test } from '@playwright/test'
import { assignProjectColors } from '@overdeck/deck-ui/project-colors'
import { startFixtureCollector, type FixtureCollector } from './fixture-collector'
import { ADAPTER_STATUSES, ALL_ITEMS, ALL_PANELS } from './fixtures/collector-fixtures'

const NOW = Date.parse('2026-07-17T12:00:00.000Z')
const SNOOZE_MS = 5_000

test.describe.configure({ mode: 'serial' })

let collector: FixtureCollector

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

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

test.beforeEach(async ({ page }) => {
  await page.addInitScript(
    ({ snoozeMs }) => {
      window.__OVERDECK_SNOOZE_MS = snoozeMs
      localStorage.removeItem('overdeck-inbox-snoozes')
      localStorage.removeItem('overdeck-project-colors')
    },
    { snoozeMs: SNOOZE_MS },
  )
})

test('renders the full triage list sorted by severity then recency', async ({ page }) => {
  await page.goto('/inbox')

  await expect(page.getByRole('heading', { level: 4, name: 'Inbox' })).toBeVisible()
  await expect(page.getByRole('button', { name: '10 actionable · sorted by impact' })).toBeVisible()

  const rows = page.locator('[data-inbox-item-id]')
  await expect(rows).toHaveCount(10)

  const ids = await rows.evaluateAll((nodes) => nodes.map((node) => node.getAttribute('data-inbox-item-id')))
  expect(ids).toEqual([
    'decision-d-4',
    'ghci:alexcodeplace/multideal:4821:1',
    'ghci:train:alexcodeplace/multideal:4901:infra-failed',
    'ghci:train:alexcodeplace/multideal:4902:cancel',
    'ghci:train:alexcodeplace/multideal:4903:green-unmerged',
    'ghci:train:alexcodeplace/multideal:4904:invalid-open',
    'halt-dep-provisioning',
    'orphan-agent',
    'limit:claude-a',
    'alert:info-note',
  ])
})

test('filter rail shows kind counts and filters the list', async ({ page }) => {
  await page.goto('/inbox')

  await expect(page.getByRole('button', { name: /All/ })).toContainText('10')
  await expect(page.getByRole('button', { name: /CI/ })).toContainText('5')
  await expect(page.getByRole('button', { name: /Alerts/ })).toContainText('2')
  await expect(page.getByRole('button', { name: /HALTs/ })).toContainText('1')
  await expect(page.getByRole('button', { name: /Decisions/ })).toContainText('1')
  await expect(page.getByRole('button', { name: /Limits/ })).toContainText('1')
  await expect(page.getByRole('button', { name: /Gates/ })).toContainText('0')

  await page.getByRole('button', { name: /CI/ }).click()
  await expect(page.locator('[data-inbox-item-id]')).toHaveCount(5)
  await expect(page.getByText('e2e failed on main')).toBeVisible()
  await expect(page.getByText('Orphan agent burning 94% CPU for 3h12m')).toHaveCount(0)

  await page.getByRole('button', { name: /Gates/ }).click()
  await expect(page.locator('[data-inbox-item-id]')).toHaveCount(0)
  await expect(page.locator('[data-inbox-empty]')).toBeVisible()
  await expect(page.getByText('nothing needs you')).toBeVisible()
})

test('snooze persists before expiry without deleting collector item', {
  tag: ['@UJ-004', '@H1', '@forbidden-side-effect', '@browser-sensitive'],
}, async ({ page }) => {
  const runtimeFailures: string[] = []
  const failedRequests: string[] = []
  page.on('console', (message) => {
    if (message.type() === 'error' || message.type() === 'warning') runtimeFailures.push(message.text())
  })
  page.on('pageerror', (error) => runtimeFailures.push(error.message))
  page.on('requestfailed', (request) => {
    if (request.url().includes('/api/collector/') && !request.url().endsWith('/api/collector/events')) {
      failedRequests.push(request.url())
    }
  })

  await page.clock.install({ time: new Date(NOW) })
  await page.goto('/inbox')

  const target = page.locator('[data-inbox-item-id="ghci:alexcodeplace/multideal:4821:1"]')
  await expect(target).toBeVisible()

  await target.getByRole('button', { name: 'Snooze' }).click()
  await expect(target).toHaveCount(0)
  await expect
    .poll(() =>
      page.evaluate(() => JSON.parse(localStorage.getItem('overdeck-inbox-snoozes') ?? '{}')),
    )
    .toEqual({ 'ghci:alexcodeplace/multideal:4821:1': NOW + SNOOZE_MS })
  expect(collector.actionCalls).toEqual([])

  await page.reload()
  await expect(target).toHaveCount(0)
  expect(runtimeFailures).toEqual([])
  expect(failedRequests).toEqual([])
})

test('expired snooze restores item and removes durable entry', {
  tag: ['@UJ-004', '@A1', '@expiry-boundary', '@browser-sensitive'],
}, async ({ page }) => {
  const runtimeFailures: string[] = []
  const failedRequests: string[] = []
  page.on('console', (message) => {
    if (message.type() === 'error' || message.type() === 'warning') runtimeFailures.push(message.text())
  })
  page.on('pageerror', (error) => runtimeFailures.push(error.message))
  page.on('requestfailed', (request) => {
    if (request.url().includes('/api/collector/') && !request.url().endsWith('/api/collector/events')) {
      failedRequests.push(request.url())
    }
  })

  await page.clock.install({ time: new Date(NOW) })
  await page.goto('/inbox')

  const target = page.locator('[data-inbox-item-id="ghci:alexcodeplace/multideal:4821:1"]')
  await expect(target).toBeVisible()

  await target.getByRole('button', { name: 'Snooze' }).click()
  await expect(target).toHaveCount(0)

  await page.clock.pauseAt((await page.evaluate(() => Date.now())) + 1_000)
  const remainingMs = await page.evaluate(() => {
    const snoozes = JSON.parse(localStorage.getItem('overdeck-inbox-snoozes') ?? '{}') as Record<string, number>
    return snoozes['ghci:alexcodeplace/multideal:4821:1']! - Date.now()
  })
  await page.clock.fastForward(remainingMs - 1)
  await expect(target).toHaveCount(0)

  await page.clock.fastForward(251)
  await expect(target).toBeVisible()
  await expect.poll(() => page.evaluate(() => localStorage.getItem('overdeck-inbox-snoozes'))).toBe('{}')

  await page.clock.resume()
  await page.reload()
  await expect(target).toBeVisible()
  expect(runtimeFailures).toEqual([])
  expect(failedRequests).toEqual([])
})

test('project color tags honor settings overrides and auto-assigned wheel colors', async ({ page }) => {
  const overrides = { 'alexcodeplace/multideal': '#ff00aa' }
  const auto = assignProjectColors(['alexcodeplace/multideal', 'dep-provisioning'], overrides)

  await page.addInitScript((value) => {
    localStorage.setItem('overdeck-project-colors', value)
  }, JSON.stringify(overrides))

  await page.goto('/inbox')

  const multidealTag = page.locator('[data-project-tag="alexcodeplace/multideal"]')
  await expect(multidealTag).toHaveCount(5)
  expect(await multidealTag.evaluateAll((nodes) => nodes.map((node) => node.getAttribute('data-project-color'))))
    .toEqual(Array(5).fill('#ff00aa'))

  const depTag = page.locator('[data-project-tag="dep-provisioning"]')
  await expect(depTag).toHaveAttribute('data-project-color', auto['dep-provisioning']!.hex)
})

test('open and train actions are enabled; unknown actions stay disabled', async ({ page }) => {
  await page.goto('/inbox')

  const openRun = page.getByRole('button', { name: 'Open run' })
  await expect(openRun).toBeEnabled()

  await expect(page.getByRole('button', { name: 'Rerun failed jobs' })).toBeEnabled()
  await expect(page.getByRole('button', { name: 'Cancel run' })).toBeEnabled()

  const rotate = page.getByRole('button', { name: 'Rotate' })
  await expect(rotate).toBeDisabled()
  await expect(rotate).toHaveAttribute('title', 'Actions land in wave 4')

  const reap = page.locator('[data-inbox-item-id="orphan-agent"]').getByRole('button', { name: 'Reap' })
  await expect(reap).toBeEnabled()

  const answer = page.locator('[data-inbox-item-id="halt-dep-provisioning"]').getByRole('link', { name: 'Answer' })
  await expect(answer).toBeVisible()
  await expect(answer).toHaveAttribute('href', '/decisions')
})

test('valid Open navigates client-side and never posts an open action', async ({ page }) => {
  const callsBefore = collector.actionCalls.length
  await page.route('https://github.com/**', (route) => route.fulfill({ status: 200, body: 'opened' }))
  await page.goto('/inbox')

  await page.locator('[data-inbox-item-id$=":green-unmerged"]').getByRole('button', { name: 'Open pull request' }).click()
  await page.waitForURL('https://github.com/alexcodeplace/multideal/pull/4903')
  expect(collector.actionCalls).toHaveLength(callsBefore)
  expect(collector.actionCalls.some((call) => call.verb === 'open')).toBe(false)
})

test('invalid Open stays on-page, shows danger feedback, and sends no POST', async ({ page }) => {
  const callsBefore = collector.actionCalls.length
  await page.goto('/inbox')

  const invalidItem = page.locator('[data-inbox-item-id="ghci:train:alexcodeplace/multideal:4904:invalid-open"]')
  await invalidItem.getByRole('button', { name: 'Open invalid link' }).click()
  await expect(page).toHaveURL(/\/inbox$/)
  const feedback = page.locator('[role="status"][aria-live="assertive"]')
  await expect(feedback).toContainText('Open failed')
  await expect(feedback).toContainText('Invalid GitHub URL')
  expect(collector.actionCalls).toHaveLength(callsBefore)
})

test('train rerun posts exact string args and shows success only after HTTP success', async ({ page }) => {
  collector.delayNextAction('ci.rerunFailed', 200)
  await page.goto('/inbox')

  await page.getByRole('button', { name: 'Rerun failed jobs' }).click()
  expect(await page.getByText('Rerun failed jobs succeeded', { exact: true }).count()).toBe(0)
  await expect(page.getByText('Rerun failed jobs succeeded', { exact: true })).toBeVisible()
  expect(collector.actionCalls.at(-1)).toEqual({
    verb: 'ci.rerunFailed',
    body: {
      args: { repo: 'alexcodeplace/multideal', runId: '9201' },
      requestedBy: 'ghci:train:alexcodeplace/multideal:4901:infra-failed',
    },
  })
})

test('train action rejection shows parsed server error and no success toast', async ({ page }) => {
  await page.goto('/inbox')

  await page.getByRole('button', { name: 'Cancel run' }).click()
  const toast = page.locator('[role="status"][aria-live="assertive"]')
  await expect(toast).toContainText('Cancel run failed')
  await expect(toast).toContainText('train action is not eligible')
  await expect(toast).not.toContainText('succeeded')
  expect(collector.actionCalls.at(-1)).toEqual({
    verb: 'ci.cancelRun',
    body: {
      args: { repo: 'alexcodeplace/multideal', runId: '9999' },
      requestedBy: 'ghci:train:alexcodeplace/multideal:4902:cancel',
    },
  })
})
