/** @vitest-environment jsdom */
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { fetchCollectorState, fetchHarnessEvents } from '../../lib/collector-client'
import type { StateResponse } from '../../lib/collector-types'
import type { ForensicsPanelData, HarnessPlanRun } from '../../lib/panel-data'
import { AgentApp, appendStreamEvent, deriveAgentSnapshot, eventTurn, selectExecutingAccount, selectSafeTaskAttempt } from './AgentApp'

vi.mock('../../lib/collector-client', async (importOriginal) => ({
  ...await importOriginal<typeof import('../../lib/collector-client')>(),
  fetchCollectorState: vi.fn(),
  fetchHarnessEvents: vi.fn(),
}))

vi.mock('../overview/CollectorRealtimeBridge', () => ({
  CollectorRealtimeBridge: () => null,
}))

const run: HarnessPlanRun = {
  runId: 'run-28',
  seq: 28,
  title: 'Optimize data integrity',
  status: 'running',
  state: 'executing',
  owner: 'operator',
  currentTask: 't4',
  tasksTotal: 2,
  tasksCompleted: 1,
  pendingDecisions: 0,
  degradedReason: '',
  updatedAt: '2026-07-18T12:00:00.000Z',
  repoRoot: '/home/user/Projects/overdeck',
  abandoned: false,
  abandonedAt: null,
  waves: [
    {
      wave: 1,
      tasks: [
        { id: 't1', status: 'succeeded', seat: 'reviewer' },
        { id: 't4', status: 'running', seat: 'coder', attempt: 2, branch: 'agent/t4-a2' },
      ],
    },
  ],
}

const forensics: ForensicsPanelData = {
  tiles: [],
  attribution: [],
  runs: [],
  segments: [
    { t0: 300, durMs: 30, cat: 'gate0', taskId: 't4', agentId: 't4.2' },
    { t0: 100, durMs: 20, cat: 'llm-implement', taskId: 't4', agentId: 't4.2' },
    { t0: 50, durMs: 10, cat: 'llm-review', taskId: 't1', agentId: 't1.1' },
  ],
}

describe('deriveAgentSnapshot', () => {
  it('joins the agent to its task and chronological timeline segments', () => {
    const snapshot = deriveAgentSnapshot(run, forensics, 't4.2')

    expect(snapshot?.task?.id).toBe('t4')
    expect(snapshot?.live).toBe(true)
    expect(snapshot?.model).toBe('')
    expect(snapshot?.turns.map((turn) => turn.at)).toEqual([100, 300])
    expect(snapshot?.knownAgentIds).toEqual(['t1.1', 't4.2'])
  })

  it('keeps paused tasks live and closes terminal tasks', () => {
    const withStatus = (status: string): HarnessPlanRun => ({
      ...run,
      waves: run.waves.map((wave) => ({
        ...wave,
        tasks: wave.tasks.map((task) => task.id === 't4' ? { ...task, status } : task),
      })),
    })

    expect(deriveAgentSnapshot(withStatus('paused'), forensics, 't4.2').live).toBe(true)
    expect(deriveAgentSnapshot(withStatus('succeeded'), forensics, 't4.2').live).toBe(false)
  })

  it('returns known agent ids for an unknown agent without inventing identity', () => {
    const snapshot = deriveAgentSnapshot(run, forensics, 'missing')
    expect(snapshot?.task).toBeNull()
    expect(snapshot?.turns).toEqual([])
    expect(snapshot?.knownAgentIds).toEqual(['t1.1', 't4.2'])
  })
})

describe('selectExecutingAccount', () => {
  it('reads account from the latest dispatch.attempt payload', () => {
    const account = selectExecutingAccount([
      {
        id: '1',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'a1',
        payload: { account: 'old' },
      },
      {
        id: '2',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:01:00.000Z',
        taskId: 't4',
        attemptId: 'a2',
        payload: { account: 'zync' },
      },
      {
        id: '3',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:02:00.000Z',
        taskId: 't1',
        attemptId: 'a3',
        payload: { account: 'other' },
      },
    ], 't4')
    expect(account).toBe('zync')
  })

  it('returns null when no journaled account exists', () => {
    expect(selectExecutingAccount([], 't4')).toBeNull()
    expect(selectExecutingAccount([
      {
        id: '1',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'a1',
        payload: {},
      },
    ], 't4')).toBeNull()
  })

  it('does not treat seat/provider as executing account', () => {
    expect(selectExecutingAccount([
      {
        id: '1',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'a1',
        payload: { seat: 'coder', provider: 'coder' },
      },
    ], 't4')).toBeNull()
  })

  it('reads account from attempt.reply when journaled by the wrapper', () => {
    expect(selectExecutingAccount([
      {
        id: '1',
        source: 'runlog',
        kind: 'attempt.prompt',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'a1',
        payload: {},
      },
      {
        id: '2',
        source: 'runlog',
        kind: 'attempt.reply',
        ts: '2026-07-22T00:01:00.000Z',
        taskId: 't4',
        attemptId: 'a1',
        payload: { account: 'seat-runner@example.com', valid: true },
      },
    ], 't4')).toBe('seat-runner@example.com')
  })
})

describe('authoritative task events', () => {
  const event = {
    id: 'opaque-1',
    source: 'runlog',
    kind: 'gate0',
    ts: '2026-07-22T00:00:00.000Z',
    taskId: 't4',
    attemptId: 'attempt-4',
    payload: { text: '<img src=x onerror=alert(1)>', durationMs: 12 },
  } as const

  it('deduplicates replayed opaque SSE ids and retains latest bounded frames', () => {
    expect(appendStreamEvent([event], event)).toEqual([event])
    expect(appendStreamEvent([event], { ...event, id: 'opaque-2' })).toHaveLength(2)
  })

  it('maps hostile authoritative payload as text without interpreting markup', () => {
    expect(eventTurn(event)).toMatchObject({
      phase: 'gate0',
      summary: '<img src=x onerror=alert(1)>',
      durMs: 12,
    })
  })

  it('does not select or mix attempts when event contract lacks lifecycle correlation', () => {
    const selected = selectSafeTaskAttempt([
      { ...event, id: 'old', attemptId: 'attempt-1' },
      { ...event, id: 'current', attemptId: 'attempt-2' },
    ], 't4', { attemptCorrelation: true })

    expect(selected).toEqual({ attemptId: null, events: [] })
  })

  it('selects only the latest explicit attempt lifecycle', () => {
    const selected = selectSafeTaskAttempt([
      { ...event, id: 'old-start', kind: 'attempt.started', attemptId: 'attempt-1', payload: { phase: 'implement' } },
      { ...event, id: 'old-event', attemptId: 'attempt-1' },
      { ...event, id: 'current-start', kind: 'attempt.started', attemptId: 'attempt-2', payload: { phase: 'review' } },
      { ...event, id: 'current-event', attemptId: 'attempt-2' },
    ], 't4', { attemptCorrelation: true })

    expect(selected.attemptId).toBe('attempt-2')
    expect(selected.events.map(({ id }) => id)).toEqual(['current-start', 'current-event'])
  })

  it('ignores lifecycle attempts with matching completion events', () => {
    const selected = selectSafeTaskAttempt([
      { ...event, id: 'start', kind: 'attempt.started', attemptId: 'attempt-2', payload: { phase: 'review' } },
      { ...event, id: 'complete', kind: 'attempt.completed', attemptId: 'attempt-2', payload: { phase: 'review' } },
    ], 't4', { attemptCorrelation: true })

    expect(selected).toEqual({ attemptId: null, events: [] })
  })

  it('does not infer an active attempt from an unambiguous historical event', () => {
    const selected = selectSafeTaskAttempt([event], 't4', { attemptCorrelation: true })

    expect(selected).toEqual({ attemptId: null, events: [] })
  })

  it('does not mix uncorrelated task history when lifecycle mapping is absent', () => {
    const selected = selectSafeTaskAttempt([{ ...event, attemptId: undefined }], 't4', undefined)

    expect(selected).toEqual({ attemptId: null, events: [] })
  })
})

describe('AgentApp stream', () => {
  afterEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('starts exact task stream after authoritative state and events load', async () => {
    const state: StateResponse = {
      panels: [
        { id: 'plans', ts: '2026-07-22T00:00:00.000Z', data: { runs: [run] } },
        { id: 'forensics:run-28', ts: '2026-07-22T00:00:00.000Z', data: forensics },
      ],
      adapters: [],
    }
    vi.mocked(fetchCollectorState).mockResolvedValue(state)
    vi.mocked(fetchHarnessEvents).mockResolvedValue({
      events: [],
      nextSince: 'opaque:0003',
      hasMore: false,
      capabilities: { attemptCorrelation: true },
    })
    const streamFetch = vi.fn((_url: string, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
      init?.signal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true })
    }))
    vi.stubGlobal('fetch', streamFetch)

    const view = render(<AgentApp runId="run-28" agentId="t4.2" />)
    try {
      await waitFor(() => expect(streamFetch).toHaveBeenCalledWith(
        '/api/collector/harness/runs/run-28/tasks/t4/stream',
        expect.objectContaining({ cache: 'no-store' }),
      ))
    } finally {
      view.unmount()
    }
  })

  it('renders journaled executing account and not-recorded fallback in the sidebar', async () => {
    const state: StateResponse = {
      panels: [
        { id: 'plans', ts: '2026-07-22T00:00:00.000Z', data: { runs: [run] } },
        { id: 'forensics:run-28', ts: '2026-07-22T00:00:00.000Z', data: forensics },
      ],
      adapters: [],
    }
    vi.mocked(fetchCollectorState).mockResolvedValue(state)
    vi.mocked(fetchHarnessEvents).mockResolvedValue({
      events: [],
      nextSince: 'opaque:0003',
      hasMore: false,
      capabilities: { attemptCorrelation: true },
    })
    vi.stubGlobal('fetch', vi.fn(() => new Promise<Response>(() => {})))

    const missing = render(<AgentApp runId="run-28" agentId="t4.2" />)
    try {
      await waitFor(() => expect(screen.getByText('not recorded')).toBeInTheDocument())
    } finally {
      missing.unmount()
    }

    vi.mocked(fetchHarnessEvents).mockResolvedValue({
      events: [{
        id: 'dispatch-1',
        source: 'runlog',
        kind: 'dispatch.attempt',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'attempt-1',
        payload: { account: 'zync', seat: 'coder' },
      }],
      nextSince: 'opaque:0004',
      hasMore: false,
      capabilities: { attemptCorrelation: true },
    })
    const recorded = render(<AgentApp runId="run-28" agentId="t4.2" />)
    try {
      await waitFor(() => expect(screen.getByText('zync')).toBeInTheDocument())
      expect(screen.queryByText('not recorded')).not.toBeInTheDocument()
    } finally {
      recorded.unmount()
    }

    vi.mocked(fetchHarnessEvents).mockResolvedValue({
      events: [{
        id: 'reply-1',
        source: 'runlog',
        kind: 'attempt.reply',
        ts: '2026-07-22T00:00:00.000Z',
        taskId: 't4',
        attemptId: 'attempt-2',
        payload: { account: 'seat-runner@example.com', valid: true },
      }],
      nextSince: 'opaque:0005',
      hasMore: false,
      capabilities: { attemptCorrelation: true },
    })
    const wrapperReported = render(<AgentApp runId="run-28" agentId="t4.2" />)
    try {
      await waitFor(() => expect(screen.getByText('seat-runner@example.com')).toBeInTheDocument())
      expect(screen.queryByText('not recorded')).not.toBeInTheDocument()
    } finally {
      wrapperReported.unmount()
    }
  })
})
