import { expect, test, type Page } from '@playwright/test'
import { startFixtureCollector, type FixtureCollector, type FixtureCollectorState } from './fixture-collector'
import {
  CI_BUILD_INCIDENT_ITEMS,
  CI_BUILD_INCIDENT_PANELS,
  CI_BUILD_OFFLOAD_ADAPTER,
  CI_PANEL_SIMULTANEOUS_INCOMPLETE,
  ciPanelWithTrainGap,
} from './fixtures/ci-build-fixtures'
import { CI_PANEL } from './fixtures/collector-fixtures'

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

let collector: FixtureCollector

const ADAPTERS = [
  CI_BUILD_OFFLOAD_ADAPTER,
  { id: 'ghci', interval: 60_000, consecutiveErrors: 0, stale: false },
]

function panelsWithCi(ciPanel: typeof CI_PANEL) {
  return CI_BUILD_INCIDENT_PANELS.map((panel) => panel.id === 'ci' ? ciPanel : panel)
}

function stateWithCi(ciPanel: typeof CI_PANEL): FixtureCollectorState {
  return { panels: panelsWithCi(ciPanel), adapters: ADAPTERS, items: CI_BUILD_INCIDENT_ITEMS }
}

async function restartWith(ciPanel: typeof CI_PANEL) {
  await collector.close()
  collector = await startFixtureCollector(stateWithCi(ciPanel))
}

function runtimeErrors(page: Page): string[] {
  const errors: string[] = []
  page.on('console', (message) => {
    if (message.type() === 'error') errors.push(message.text())
  })
  page.on('pageerror', (error) => errors.push(error.message))
  return errors
}

test.beforeAll(async () => {
  collector = await startFixtureCollector(stateWithCi(CI_PANEL))
})

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

test('renders all-scope trains, cross-repo PRs, runners, and amplification with honest gaps', async ({ page }) => {
  const errors = runtimeErrors(page)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })

  await expect(page.getByRole('region', { name: 'Repository CI scope' })).toBeVisible()
  await expect(page.getByRole('region', { name: 'Merge trains' })).toBeVisible()
  await expect(page.getByRole('region', { name: 'Runner fleet' })).toHaveCount(1)
  await expect(page.getByRole('article', { name: 'Runner laptop-runner' })).toBeVisible()
  await expect(page.getByRole('article', { name: 'Runner debian-runner' })).toBeVisible()
  await expect(page.getByRole('region', { name: /CI amplification for/ })).toHaveCount(2)
  await expect(page.getByRole('table', { name: 'PR queue' }).getByRole('columnheader', { name: 'Repo' })).toBeVisible()
  await expect(page.locator('[data-testid^="offload-kpi-"]')).toHaveCount(6)
  await expect(page.getByTestId('offload-kpi-oldestQueuedMs')).not.toContainText('not recorded')
  await expect(page.getByTestId('offload-kpi-exit127')).toContainText('37')
  await expect(page.getByTestId('offload-kpi-exit127')).toContainText('host debian1')
  await expect(page.getByTestId('offload-kpi-remote24h')).not.toContainText('not recorded')
  await expect(page.getByTestId('offload-kpi-remoteSuccessPct')).not.toContainText('not recorded')
  await expect(page.getByTestId('offload-kpi-epochDiscarded')).toContainText('not recorded')
  await expect(page.getByTestId('offload-kpi-longestPullMs')).toContainText('not recorded')

  const multidealTrain = page.locator('[data-testid="train-card"][data-branch="integration/batch-train-12"]')
  await expect(multidealTrain.getByRole('link', { name: '#79' }))
    .toHaveAttribute('href', 'https://github.com/alexcodeplace/multideal/pull/79')
  const prQueue = page.getByRole('table', { name: 'PR queue' })
  await expect(prQueue.locator('a[href="https://github.com/alexcodeplace/multideal/pull/79"]'))
    .toHaveAttribute('data-pr-identity', 'alexcodeplace/multideal#79')
  await expect(prQueue.locator('a[href="https://github.com/alexcodeplace/overdeck/pull/79"]'))
    .toHaveAttribute('data-pr-identity', 'alexcodeplace/overdeck#79')

  const incompleteJobs = page.locator('[data-testid="train-card"][data-branch="integration/batch-train-13"]')
  await expect(incompleteJobs).toContainText('jobs incomplete')
  await expect(incompleteJobs).not.toContainText('failure classification')
  await expect(incompleteJobs).not.toContainText('recommended')
  expect(errors).toEqual([])
})

test('repo scope filters every train dashboard model and hides the Repo column', async ({ page }) => {
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })
  await page.getByRole('button', { name: /^alexcodeplace\/overdeck / }).click()

  await expect(page.locator('[data-testid="train-card"]')).toHaveCount(1)
  await expect(page.locator('[data-testid="train-card"]')).toHaveAttribute('data-repo', 'alexcodeplace/overdeck')
  await expect(page.getByTestId('pr-queue-table-scroll').getByRole('columnheader', { name: 'Repo' })).toHaveCount(0)
  await expect(page.getByTestId('pr-queue-row-alexcodeplace-overdeck-79')).toBeVisible()
  await expect(page.getByTestId('pr-queue-row-alexcodeplace-multideal-79')).toHaveCount(0)
  await expect(page.getByRole('region', { name: 'Runner fleet' })).toHaveCount(1)
  await expect(page.getByRole('article', { name: 'Runner laptop-runner' })).toBeVisible()
  await expect(page.getByRole('article', { name: 'Runner debian-runner' })).toBeVisible()
  await expect(page.getByRole('region', { name: 'CI amplification for alexcodeplace/overdeck' })).toBeVisible()
})

test('train mutations post exact verbs and string args; success waits for HTTP and ToastProvider stays valid', async ({ page }) => {
  const errors = runtimeErrors(page)
  collector.delayNextAction('ci.rerunFailed', 200)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })

  const failed = page.locator('[data-testid="train-card"][data-branch="integration/batch-train-12"]')
  await failed.getByRole('button', { name: 'Rerun failed' }).click()
  await expect(page.getByText('Rerun failed succeeded', { exact: true })).toHaveCount(0)
  await expect(page.getByText('Rerun failed succeeded', { exact: true })).toBeVisible()
  expect(collector.actionCalls.at(-1)).toEqual({
    verb: 'ci.rerunFailed',
    body: {
      args: { repo: 'alexcodeplace/multideal', runId: '9201' },
      requestedBy: 'overdeck-web',
    },
  })

  const gating = page.locator('[data-testid="train-card"][data-branch="integration/batch-train-13"]')
  await gating.getByRole('button', { name: 'Cancel superseded' }).click()
  await expect(page.getByText('Cancel superseded succeeded', { exact: true })).toBeVisible()
  expect(collector.actionCalls.at(-1)).toEqual({
    verb: 'ci.cancelRun',
    body: {
      args: { repo: 'alexcodeplace/multideal', runId: '9202' },
      requestedBy: 'overdeck-web',
    },
  })
  expect(errors).toEqual([])
})

test('Open performs validated client navigation with no action POST', async ({ page }) => {
  const callsBefore = collector.actionCalls.length
  await page.route('https://github.com/**', (route) => route.fulfill({ status: 200, body: 'opened' }))
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })

  const green = page.locator('[data-testid="train-card"][data-repo="alexcodeplace/overdeck"]')
  await green.getByRole('button', { name: 'Open' }).click()
  await page.waitForURL('https://github.com/alexcodeplace/overdeck/pull/5901')
  expect(collector.actionCalls).toHaveLength(callsBefore)
  expect(collector.actionCalls.some((call) => call.verb === 'open')).toBe(false)
})

test('stale train mutation surfaces exact parsed server error without optimistic success', async ({ page }) => {
  const stale = structuredClone(CI_PANEL)
  const data = stale.data as {
    repos: Array<{ repo: string; trains: Array<{ branch: string; gateRun: { status: string } | null }> }>
  }
  const repo = data.repos.find((entry) => entry.repo === 'alexcodeplace/multideal')!
  const train = repo.trains.find((entry) => entry.branch === 'integration/batch-train-13')!
  train.gateRun!.status = 'completed'
  await restartWith(stale)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })

  const gating = page.locator('[data-testid="train-card"][data-branch="integration/batch-train-13"]')
  await gating.getByRole('button', { name: 'Cancel superseded' }).click()
  await expect(page.getByText('Cancel superseded failed', { exact: true })).toBeVisible()
  await expect(page.getByText('train action is not eligible', { exact: true })).toBeVisible()
  await expect(page.getByText('Cancel superseded succeeded', { exact: true })).toHaveCount(0)
})

test('cached trains preserve each independent completeness gap and expose no actions', async ({ page }) => {
  const cases = [
    ['refsComplete', 'train refs incomplete'],
    ['prsComplete', 'PRs incomplete'],
    ['trainRunsComplete', 'train runs incomplete'],
    ['trainsComplete', 'train data incomplete'],
  ] as const

  for (const [field, gap] of cases) {
    await restartWith(ciPanelWithTrainGap(field, gap))
    await page.goto('/ci', { waitUntil: 'domcontentloaded' })
    await page.getByRole('button', { name: /^alexcodeplace\/multideal / }).click()
    const cards = page.locator('[data-testid="train-card"]')
    await expect(cards).toHaveCount(2)
    await expect(cards.first()).toContainText(gap)
    await expect(cards.getByRole('button', { name: /Rerun failed|Cancel superseded|Open/ })).toHaveCount(0)
  }
})

test('simultaneous incompleteness reaches every named component without false totals', async ({ page }) => {
  await restartWith(CI_PANEL_SIMULTANEOUS_INCOMPLETE)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })
  const repoScope = page.getByRole('button', { name: /^alexcodeplace\/multideal / })
  await repoScope.click()

  await expect(repoScope).toContainText('Open PR list truncated')
  await expect(repoScope).toContainText('Runner occupancy unavailable')
  await expect(repoScope).toContainText('Actions queue unavailable')
  await expect(repoScope).toContainText('Train branch list truncated')
  await expect(page.getByText('PR list may be incomplete').first()).toBeVisible()
  await expect(page.getByText('checks incomplete').first()).toBeVisible()
  await expect(page.getByText('Runner fleet incomplete')).toBeVisible()
  await expect(page.getByText('Run history incomplete — warming up')).toBeVisible()
  await expect(page.getByText('alexcodeplace/multideal: Train branch list truncated')).toBeVisible()
  await expect(repoScope).not.toContainText('null')
})

test('missing selected repo after SSE refresh resets scope to All', async ({ page }) => {
  await restartWith(CI_PANEL)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })
  await page.getByRole('button', { name: /^alexcodeplace\/overdeck / }).click()
  await expect(page.getByRole('button', { name: 'All repositories' })).toHaveAttribute('aria-pressed', 'false')

  const reduced = structuredClone(CI_PANEL)
  const data = reduced.data as { repos: Array<{ repo: string }> }
  data.repos = data.repos.filter((repo) => repo.repo === 'alexcodeplace/multideal')
  await collector.waitForClient()
  collector.emit({ type: 'panel', panel: reduced })

  await expect(page.getByRole('button', { name: 'All repositories' })).toHaveAttribute('aria-pressed', 'true')
  await expect(page.locator('[data-testid="train-card"]')).toHaveCount(2)
})

test('renders train dashboard in light theme', async ({ page }) => {
  await restartWith(CI_PANEL)
  await page.goto('/ci', { waitUntil: 'domcontentloaded' })
  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('region', { name: 'Merge trains' })).toBeVisible()
  await expect(page.getByRole('region', { name: 'Repository CI scope' })).toBeVisible()
})
