import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { ClusterSnapshot } from '../../lib/cluster-types'
import { COLLECTOR_POLL_MS } from '../../lib/collector-queries'
import { ClusterContent, ClusterPanel, sessionHref } from './ClusterContent'
import { fetchCluster, fetchCollectorState } from '../../lib/collector-client'
import { CLUSTER_QUEUE_PANEL, FLEET_PANEL, OFFLOAD_CONTROL_PANEL, REMOTE_JOBS_PANEL } from '../../../tests/fixtures/ci-build-fixtures'

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

const SNAPSHOT: ClusterSnapshot = {
  observedAt: '2026-08-08T10:00:00.000Z',
  cluster: { id: 'local-k3s', version: 'v1.33.1', state: 'degraded' },
  sources: {
    kubernetes: { status: 'present', observedAt: '2026-08-08T10:00:00.000Z' },
    prometheus: { status: 'absent', error: 'Prometheus refused the query' },
    ledger: { status: 'present', observedAt: '2026-08-08T09:59:59.000Z' },
    builds: { status: 'partial', scopes: { complete: ['current'], incomplete: ['history'] } },
  },
  summary: {
    readyNodes: 1, totalNodes: 2, runningWorkloads: 3, pendingWorkloads: null,
    failedWorkloads: 1, requestedCpuCores: null, requestedMemoryBytes: null,
    usedCpuCores: null, usedMemoryBytes: null,
  },
  nodes: [{
    name: 'builder-1', uid: 'node-1', ready: true,
    conditions: [{ type: 'Ready', status: 'True', reason: null, observedAt: '2026-08-08T10:00:00.000Z' }],
    taints: [], capacity: { cpuCores: 8, memoryBytes: 16 * 1024 ** 3 },
    allocatable: { cpuCores: 7.5, memoryBytes: 15 * 1024 ** 3 },
    utilization: { cpuCores: null, memoryBytes: null }, requests: { cpuCores: 2, memoryBytes: 4 * 1024 ** 3 },
    limits: { cpuCores: null, memoryBytes: null }, workloadCounts: { running: 1, pending: 0, failed: 0 }, restartCount: null,
    sessions: [], builds: [], workloads: [], sourceTimestamps: { kubernetes: '2026-08-08T10:00:00.000Z' },
    coverageGaps: [{ source: 'prometheus', reason: 'Prometheus refused the query' }],
  }],
  unplacedSessions: [], unmatchedWorkloads: [], unplacedBuilds: [],
  links: { grafana: 'https://grafana.example/d/node', headlamp: 'javascript:alert(1)' },
}

afterEach(() => { vi.useRealTimers(); cleanup(); vi.clearAllMocks(); vi.restoreAllMocks() })

function renderPanel() {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
  return render(<QueryClientProvider client={client}><ClusterPanel snapshot={SNAPSHOT} /></QueryClientProvider>)
}

function renderClusterContent() {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
  return render(<QueryClientProvider client={client}><ClusterContent /></QueryClientProvider>)
}

describe('ClusterContent', () => {
  it('keeps rendered last-good machines when the production refresh fails', async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true })
    vi.mocked(fetchCluster).mockResolvedValue(SNAPSHOT)
    vi.mocked(fetchCollectorState)
      .mockResolvedValueOnce({ panels: [FLEET_PANEL], adapters: [] })
      .mockRejectedValueOnce(new Error('refresh failed'))

    renderClusterContent()
    expect(await screen.findByTestId('machine-card-debian1')).toBeInTheDocument()

    await act(async () => { await vi.advanceTimersByTimeAsync(COLLECTOR_POLL_MS) })
    await waitFor(() => expect(fetchCollectorState).toHaveBeenCalledTimes(2))
    expect(screen.getByTestId('machine-card-debian1')).toBeInTheDocument()
    expect(screen.getByTestId('machine-card-debian2')).toBeInTheDocument()
  })

  it('uses the shared live Machines surface ahead of the cluster summary', async () => {
    vi.mocked(fetchCluster).mockResolvedValue(SNAPSHOT)
    vi.mocked(fetchCollectorState).mockResolvedValue({
      panels: [
        OFFLOAD_CONTROL_PANEL,
        CLUSTER_QUEUE_PANEL,
        REMOTE_JOBS_PANEL,
        { ...FLEET_PANEL, data: { ...(FLEET_PANEL.data as { hosts: Array<{ host: string }> }), hosts: [...(FLEET_PANEL.data as { hosts: Array<{ host: string }> }).hosts, { ...(FLEET_PANEL.data as { hosts: Array<{ host: string }> }).hosts[1], host: 'debian3' }] } },
      ],
      adapters: [],
    })
    renderClusterContent()

    const fleet = await screen.findByTestId('ci-build-fleet')
    expect(within(fleet).getByTestId('machine-card-debian1')).toBeInTheDocument()
    expect(within(fleet).getByTestId('machine-card-debian2')).toBeInTheDocument()
    expect(within(fleet).getByTestId('machine-card-debian3')).toBeInTheDocument()
    expect(fleet.compareDocumentPosition(screen.getByText('local-k3s cluster')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
    expect(fetchCollectorState).toHaveBeenCalledTimes(1)
    expect(fetchCluster).toHaveBeenCalledTimes(1)
  })
})

describe('ClusterPanel', () => {
  it('uses only a recorded run id for the canonical observability link', () => {
    const base = { sessionId: 's1', confidence: 'exact' as const, evidence: [], gaps: [], session: { id: 's1', runtime: 'codex', cwd: '/repo', host: 'builder-1', state: 'ALIVE-WORKING', title: null, activity: null } }
    expect(sessionHref({ ...base, session: { ...base.session, runId: 'run/1' } })).toBe('/plans/run%2F1')
    expect(sessionHref({ ...base, buildKey: 'plausible-run-name' })).toBeNull()
  })

  it('renders source reasons and never turns absent metrics into zero', () => {
    renderPanel()
    expect(screen.getAllByText('Prometheus refused the query').length).toBeGreaterThan(0)
    expect(screen.getAllByText('Incomplete scopes: history').length).toBeGreaterThan(0)
    expect(screen.queryByText('Pending workloads')).not.toBeInTheDocument()
    const row = screen.getByRole('row', { name: /builder-1/i })
    expect(within(row).getAllByText('—').length).toBeGreaterThanOrEqual(2)
    expect(screen.getByRole('button', { name: 'Open in Grafana' })).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: 'Open in Headlamp' })).not.toBeInTheDocument()
  })

  it('opens node detail through the node query route', async () => {
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.json({ observedAt: SNAPSHOT.observedAt, sources: SNAPSHOT.sources, node: SNAPSHOT.nodes[0] }))
    renderPanel()
    fireEvent.click(screen.getByRole('button', { name: 'builder-1' }))
    expect(screen.getByRole('dialog')).toHaveTextContent('builder-1')
    expect(await screen.findByText('Capacity CPU')).toBeInTheDocument()
    expect(globalThis.fetch).toHaveBeenCalledWith('/api/collector/cluster/nodes/builder-1')
  })
})
