import { planStatusCategory } from '@overdeck/deck-ui'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import { describe, expect, it } from 'vitest'
import type { HarnessPlanRun, HarnessQueueEntry } from '../../lib/panel-data'
import {
  buildProjectGroups,
  canonicalRepoRoot,
  planTableItemForRun,
  projectLabelForRun,
  projectRunsForRepo,
} from './plan-groups'
import {
  PLANS_VIEWPORT_LAYOUT_CLASS,
  QueueSection,
  queueRowsForEntries,
} from './PlansContent'

function planRun(overrides: Partial<HarnessPlanRun> = {}): HarnessPlanRun {
  return {
    runId: 'run-28',
    seq: 28,
    title: 'Optimize data integrity',
    status: 'running',
    state: 'executing',
    owner: 'operator',
    currentTask: 't2',
    tasksTotal: 3,
    tasksCompleted: 1,
    pendingDecisions: 0,
    degradedReason: '',
    updatedAt: '2026-07-18T12:00:00.000Z',
    repoRoot: '/home/user/Projects/overdeck',
    registry: {
      slug: 'optimize-data-integrity',
      created: '2026-07-18',
      projectName: 'Overdeck',
    },
    abandoned: false,
    abandonedAt: null,
    waves: [
      {
        wave: 1,
        tasks: [
          { id: 't1', status: 'succeeded' },
          { id: 't2', status: 'running' },
          { id: 't3', status: 'running' },
        ],
      },
    ],
    ...overrides,
  }
}

function queueEntry(overrides: Partial<HarnessQueueEntry> = {}): HarnessQueueEntry {
  return {
    id: 'queue-1',
    repo: '/home/user/Projects/overdeck',
    slug: 'phase-three',
    preset: null,
    account: null,
    addedAt: '2026-07-30T00:00:00.000Z',
    status: 'pending',
    attempt: 0,
    runId: null,
    window: null,
    nextEligibleAt: null,
    bypassRequestedAt: null,
    terminalAt: null,
    failure: null,
    ...overrides,
  }
}

describe('plans project grouping', () => {
  it('pins a viewport-fit full-width horizontal split', () => {
    expect(PLANS_VIEWPORT_LAYOUT_CLASS).toContain('grid-cols-1')
    expect(PLANS_VIEWPORT_LAYOUT_CLASS).toContain('grid-rows-[auto_minmax(0,1fr)_minmax(0,1fr)]')
    expect(PLANS_VIEWPORT_LAYOUT_CLASS).toContain('overflow-hidden')
  })

  it('propagates alarmed onto plan table items when the run is stalled', () => {
    expect(planTableItemForRun(planRun({ alarmed: true }))).toMatchObject({ alarmed: true })
    expect(planTableItemForRun(planRun())).not.toHaveProperty('alarmed')
  })

  it('maps only recorded row fields and preserves needs-you separately from failure', () => {
    const run = planRun({ status: 'failed', pendingDecisions: 1, failClass: 'gate-failed' })

    expect(planTableItemForRun(run)).toEqual({
      activeAgents: 2,
      createdDate: '2026-07-18',
      failClass: 'gate-failed',
      href: '/plans/run-28',
      needsYou: true,
      runId: 'run-28',
      seq: 28,
      slug: 'optimize-data-integrity',
      status: 'failed',
      tasksCompleted: 1,
      tasksTotal: 3,
      title: 'Optimize data integrity',
      updatedAtMs: Date.parse('2026-07-18T12:00:00.000Z'),
      runningStartedAtMs: null,
    })
  })

  it('uses honest project label fallbacks', () => {
    expect(projectLabelForRun(planRun())).toBe('Overdeck')
    expect(projectLabelForRun(planRun({ registry: undefined }))).toBe('overdeck')
    expect(projectLabelForRun(planRun({ repoRoot: '', registry: undefined }))).toBe('unassigned')
  })

  it('excludes abandoned runs and sorts groups attention-first', () => {
    const groups = buildProjectGroups([
      planRun({ runId: 'quiet', repoRoot: '/repos/quiet', registry: { projectName: 'Quiet' } }),
      planRun({ runId: 'attention', repoRoot: '/repos/attention', registry: { projectName: 'Attention' }, pendingDecisions: 2 }),
      planRun({ runId: 'hidden', abandoned: true }),
    ])

    expect(groups.map((group) => group.summary.label)).toEqual(['Attention', 'Quiet'])
    expect(groups[0]?.summary).toMatchObject({ plans: 1, running: 0, needYou: 1, failed: 0 })
    expect(groups.flatMap((group) => group.runs).some((run) => run.runId === 'hidden')).toBe(false)
  })

  it('folds one plan re-run across repo roots into a single row', () => {
    const groups = buildProjectGroups([
      planRun({
        runId: 'clone',
        status: 'failed',
        repoRoot: '/tmp/rp-iso/overdeck/repo',
        updatedAt: '2026-07-18T09:00:00.000Z',
      }),
      planRun({
        runId: 'worktree',
        repoRoot: '/home/user/Projects/overdeck/.worktrees/fix',
        updatedAt: '2026-07-18T12:00:00.000Z',
      }),
    ])

    expect(groups).toHaveLength(1)
    expect(groups[0]?.summary).toMatchObject({ key: 'Overdeck', label: 'Overdeck', plans: 1 })
    expect(groups[0]?.runs.map((run) => run.runId)).toEqual(['worktree'])
    expect(planTableItemForRun(groups[0]!.runs[0]!).attempts).toBe(2)
  })

  it('keeps distinct plans in one project apart', () => {
    const groups = buildProjectGroups([
      planRun({ runId: 'a', registry: { slug: 'plan-a', projectName: 'Overdeck' } }),
      planRun({ runId: 'b', registry: { slug: 'plan-b', projectName: 'Overdeck' } }),
    ])

    expect(groups).toHaveLength(1)
    expect(groups[0]?.summary.plans).toBe(2)
  })

  it('sums engine-reported attempts across folded checkouts', () => {
    const groups = buildProjectGroups([
      planRun({
        runId: 'first',
        status: 'failed',
        attempts: 4,
        repoRoot: '/home/user/Projects/overdeck/.product-work',
      }),
      planRun({ runId: 'second', attempts: 7, updatedAt: '2026-07-19T12:00:00.000Z' }),
    ])

    expect(planTableItemForRun(groups[0]!.runs[0]!)).toMatchObject({ runId: 'second', attempts: 11 })
  })

  it('keeps concurrently live runs on their own rows and collapses their finished attempts', () => {
    const groups = buildProjectGroups([
      planRun({ runId: 'main-checkout', status: 'running' }),
      planRun({
        runId: 'worktree-checkout',
        status: 'running',
        repoRoot: '/home/user/Projects/overdeck/.worktrees/fix',
        updatedAt: '2026-07-19T12:00:00.000Z',
      }),
      planRun({ runId: 'old-attempt', status: 'failed', attempts: 3, updatedAt: '2026-07-17T12:00:00.000Z' }),
      planRun({ runId: 'older-attempt', status: 'failed', updatedAt: '2026-07-16T12:00:00.000Z' }),
    ])

    expect(groups[0]?.runs.map((run) => run.runId).sort()).toEqual([
      'main-checkout',
      'old-attempt',
      'worktree-checkout',
    ])
    expect(planTableItemForRun(groups[0]!.runs.find((run) => run.runId === 'old-attempt')!).attempts).toBe(4)
    expect(groups[0]?.summary.plans).toBe(1)
  })

  it('lets the live run speak for its plan even when a finished attempt is stamped later', () => {
    const groups = buildProjectGroups([
      planRun({ runId: 'live', status: 'running', updatedAt: '2026-07-17T12:00:00.000Z' }),
      planRun({ runId: 'finished-later', status: 'failed', updatedAt: '2026-07-19T12:00:00.000Z' }),
    ])

    expect(groups[0]?.runs.map((run) => run.runId)).toEqual(['live'])
    expect(planTableItemForRun(groups[0]!.runs[0]!).attempts).toBe(2)
  })

  it('resolves detached checkouts to the project that owns them', () => {
    expect(canonicalRepoRoot('/home/user/Projects/overdeck/.worktrees/fix')).toBe('/home/user/Projects/overdeck')
    expect(canonicalRepoRoot('/home/user/Projects/mod/.product-work/docs')).toBe('/home/user/Projects/mod')
    expect(canonicalRepoRoot('/home/user/Projects/overdeck/.wt-obs-b3')).toBe('/home/user/Projects/overdeck')
    expect(canonicalRepoRoot('/home/user/Projects/overdeck')).toBe('/home/user/Projects/overdeck')
    expect(canonicalRepoRoot('/home/user/Projects/overdeck/.claude/worktrees/fix')).toBe('/home/user/Projects/overdeck')
  })

  it('returns active runs associated with a scoreboard repo, attention first', () => {
    const runs = projectRunsForRepo([
      planRun({ runId: 'complete', status: 'succeeded', registry: { projectName: 'Multideal' } }),
      planRun({ runId: 'running', registry: { projectName: 'MULTIDEAL' } }),
      planRun({ runId: 'attention', pendingDecisions: 1, registry: { projectName: 'multideal' } }),
      planRun({ runId: 'other', registry: { projectName: 'Overdeck' } }),
      planRun({ runId: 'abandoned', abandoned: true, registry: { projectName: 'multideal' } }),
    ], 'multideal')

    expect(runs.map((run) => run.runId)).toEqual(['attention', 'running', 'complete'])
  })

  it('maps null and invalid activity timestamps to honest gaps', () => {
    expect(planTableItemForRun(planRun({ updatedAt: null })).updatedAtMs).toBeNull()
    expect(planTableItemForRun(planRun({ updatedAt: 'invalid' })).updatedAtMs).toBeNull()
  })

  it('omits a zero sequence because the harness value is meaningless', () => {
    expect(planTableItemForRun(planRun({ seq: 0 }))).not.toHaveProperty('seq')
  })

  it('counts every seat the engine reports as occupied, not just the v1 spelling', () => {
    const waves = [{
      wave: 1,
      tasks: [
        { id: 't1', status: 'working' },
        { id: 't2', status: 'verifying' },
        { id: 't3', status: 'reviewing' },
        { id: 't4', status: 'succeeded' },
        { id: 't5', status: 'pending' },
      ],
    }]

    expect(planTableItemForRun(planRun({ waves })).activeAgents).toBe(3)
  })

  it('never times a running plan from the plan authoring date', () => {
    const activityStart = '2026-07-18T14:30:00.000Z'
    expect(planTableItemForRun(planRun({ status: 'running', currentActivityStartedAt: activityStart })).runningStartedAtMs).toBe(Date.parse(activityStart))
    expect(planTableItemForRun(planRun({ status: 'running' })).runningStartedAtMs).toBeNull()
    expect(planTableItemForRun(planRun({ status: 'failed' })).runningStartedAtMs).toBeNull()
  })

  it('carries the collapsed-attempts count only when the plan relaunched', () => {
    expect(planTableItemForRun(planRun({ attempts: 11 })).attempts).toBe(11)
    expect(planTableItemForRun(planRun({ attempts: 1 }))).not.toHaveProperty('attempts')
    expect(planTableItemForRun(planRun())).not.toHaveProperty('attempts')
  })

  it('sorts runs inside each group with active plans first', () => {
    const groups = buildProjectGroups([
      planRun({
        runId: 'failed',
        status: 'failed',
        updatedAt: '2026-07-21T12:00:00.000Z',
        registry: { projectName: 'Overdeck' },
      }),
      planRun({
        runId: 'running',
        status: 'running',
        tasksCompleted: 2,
        tasksTotal: 4,
        updatedAt: '2026-07-21T08:00:00.000Z',
        registry: { projectName: 'Overdeck' },
      }),
    ])

    expect(groups[0]?.runs.map((run) => run.runId)).toEqual(['running', 'failed'])
  })

  it('aggregates group counts from the same categories rendered by row chips', () => {
    const group = buildProjectGroups([
      planRun({ runId: 'attention', status: 'running', pendingDecisions: 1 }),
      planRun({ runId: 'failed', status: 'failed' }),
      planRun({ runId: 'complete', status: 'succeeded' }),
    ])[0]!
    const categories = group.runs.map((run) => {
      const item = planTableItemForRun(run)
      return planStatusCategory(item.status, item.needsYou)
    })

    expect(group.summary).toMatchObject({
      running: categories.filter((category) => category === 'running').length,
      needYou: categories.filter((category) => category === 'needs-you').length,
      failed: categories.filter((category) => category === 'failed').length,
    })
  })

  it('preserves queue source order and source statuses before any plan runs exist', () => {
    const rows = queueRowsForEntries([
      queueEntry({ id: 'pending', status: 'pending' }),
      queueEntry({ id: 'window-held', status: 'pending', window: { source: 'night', start: '22:00', end: '06:00', time_zone: 'UTC' } }),
      queueEntry({ id: 'running', status: 'running', runId: 'run-active' }),
      queueEntry({ id: 'done', status: 'done' }),
      queueEntry({ id: 'failed', status: 'failed' }),
      queueEntry({ id: 'skipped', status: 'skipped' }),
    ])

    expect(rows.map((row) => row.id)).toEqual(['pending', 'window-held', 'running', 'done', 'failed', 'skipped'])
    expect(rows.map((row) => row.status)).toEqual(['pending', 'pending', 'running', 'done', 'failed', 'skipped'])
    expect(rows.map((row) => row.position)).toEqual([1, 2, 3, 4, 5, 6])
    expect(rows.find((row) => row.id === 'running')).toMatchObject({ runHref: '/plans/run-active' })
  })

  it('keeps queue optionals honest and does not create row actions', () => {
    const [row] = queueRowsForEntries([queueEntry({
      repo: '/tmp/worktrees/queue-repo',
      window: { source: 'night', start: '22:00', end: '06:00', time_zone: 'UTC' },
      nextEligibleAt: '2026-07-30T22:00:00.000Z',
    })])

    expect(row).toMatchObject({
      repository: 'queue-repo',
      repositoryPath: '/tmp/worktrees/queue-repo',
      runHref: null,
      nextEligibleAt: '2026-07-30T22:00:00.000Z',
      window: 'night 22:00–06:00 UTC',
    })
    expect(row).not.toHaveProperty('actions')
    expect(queueRowsForEntries([queueEntry({ window: null, nextEligibleAt: null, runId: null })])[0]).toMatchObject({
      window: null,
      nextEligibleAt: null,
      runHref: null,
    })
  })

  it('reorders queue rows when a sortable header is activated', async () => {
    render(<QueueSection entries={[
      queueEntry({ id: 'first', repo: '/repos/zulu' }),
      queueEntry({ id: 'second', repo: '/repos/alpha' }),
    ]} />)
    const table = screen.getByRole('table', { name: 'Harness plans queue' })
    const repositories = () => within(table).getAllByRole('row').slice(1).map((row) => within(row).getAllByRole('cell')[1]?.textContent)
    expect(repositories()).toEqual(['zulu', 'alpha'])

    fireEvent.click(within(table).getByRole('button', { name: /Repository/ }))
    await waitFor(() => expect(repositories()).toEqual(['alpha', 'zulu']))
  })

})
