/** @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ACTIVITY_SEVERITY_ORDER, type ActivityCategory, type ActivityEvent, type ActivityReadResponse, type ActivitySourceCoverage } from '../../lib/activity-types'
import { planStatusCategory } from '@overdeck/deck-ui'
import { LogsContentBody } from './LogsContent'

function event(overrides: Partial<ActivityEvent> = {}): ActivityEvent {
  return {
    id: 'event-1',
    ts: '2026-08-08T10:00:00.000Z',
    category: 'run',
    source: 'agent',
    actor: 'agent',
    severity: 'info',
    title: 'Default event',
    ...overrides,
  }
}

const baseCoverage: ActivitySourceCoverage = {
  id: 'source-a',
  label: 'agent-source',
  category: 'agent',
  path: '/var/overdeck/agent',
  authority: 'authoritative',
  storage: 'test source',
  retention: 'Source-managed; earliest and latest observed records shown.',
  queryBounds: 'test bounds',
  status: 'ok',
  records: 3,
  totalRecords: 4,
  earliest: '2026-08-08T08:00:00.000Z',
  latest: '2026-08-08T10:00:00.000Z',
  skipped: 0,
}

function response(overrides: Partial<ActivityReadResponse> = {}): ActivityReadResponse {
  return {
    events: [],
    total: 0,
    truncated: false,
    limit: 500,
    categoryCounts: [],
    projectCounts: [],
    sourceCounts: [],
    series: [],
    coverage: [],
    suppressedNotifications: {
      status: 'ok',
      path: '/var/overdeck/notifications',
      rows: [],
      sourceCount: 0,
      attemptCount: 0,
      skipped: 0,
    },
    ...overrides,
  }
}

function renderBody(overrides: Partial<Parameters<typeof LogsContentBody>[0]> = {}) {
  return render(
    <LogsContentBody
      data={overrides.data ?? response()}
      errorTotal={overrides.errorTotal}
      range={overrides.range ?? '1d'}
      category={overrides.category}
      project={overrides.project}
      severityFloor={overrides.severityFloor}
      grouping={overrides.grouping ?? 'on'}
      searchText={overrides.searchText ?? ''}
      onRange={overrides.onRange ?? vi.fn()}
      onCategory={overrides.onCategory ?? vi.fn()}
      onProject={overrides.onProject ?? vi.fn()}
      onSeverity={overrides.onSeverity ?? vi.fn()}
      onGrouping={overrides.onGrouping ?? vi.fn()}
      onSearch={overrides.onSearch ?? vi.fn()}
      selectedEventId={overrides.selectedEventId ?? null}
      onSelectEvent={overrides.onSelectEvent ?? vi.fn()}
    />,
  )
}

function eventRowKeys(): string[] {
  return Array.from(screen.getByRole('table', { name: 'Activity events' }).querySelectorAll('[data-event-row]'))
    .map((cell) => cell.getAttribute('data-event-row') as string)
}

function eventKpiValue(label: string): string {
  const labelElements = screen.getAllByText(label)
  const kpi = labelElements.find((entry) => entry.closest('table') === null)
  expect(kpi).toBeDefined()
  expect(kpi!.previousElementSibling).not.toBeNull()
  return (kpi!.previousElementSibling as HTMLElement).textContent ?? ''
}

function sourceRows(): HTMLTableRowElement[] {
  const table = screen.getByRole('table', { name: 'Activity source coverage' })
  return within(table)
    .getAllByRole('row')
    .filter((row) => row.querySelector('th') === null)
    .map((row) => row as HTMLTableRowElement)
}

function suppressedTableRows(): HTMLTableRowElement[] {
  const table = screen.getByRole('table', { name: 'Suppressed notifications' })
  return within(table)
    .getAllByRole('row')
    .filter((row) => row.querySelector('th') === null)
    .map((row) => row as HTMLTableRowElement)
}

describe('LogsContentBody', () => {
  beforeEach(() => {
    vi.setSystemTime(new Date('2026-08-08T11:00:00.000Z'))
  })

  it('reports events-in-range KPI from total and not from visible row count', () => {
    renderBody({
      data: response({
        events: [event(), event({ id: 'event-2' }), event({ id: 'event-3' })],
        total: 16658,
        coverage: [baseCoverage],
      }),
    })

    expect(eventKpiValue('Events in range')).toBe('16,658')
  })

  it('uses the activity response error count and omits the KPI for an older response', () => {
    const fixture = renderBody({
      data: response({ events: [event()], total: 10, coverage: [baseCoverage], errorCount: 42 }),
      errorTotal: 42,
    })

    expect(eventKpiValue('Errors in range')).toBe('42')

    fixture.rerender(
      <LogsContentBody
        data={response({ events: [event()], total: 10, coverage: [baseCoverage] })}
        errorTotal={undefined}
        range="1d"
        category={undefined}
        project={undefined}
        severityFloor={undefined}
        grouping="on"
        searchText=""
        onRange={vi.fn()}
        onCategory={vi.fn()}
        onProject={vi.fn()}
        onSeverity={vi.fn()}
        onGrouping={vi.fn()}
        onSearch={vi.fn()}
        selectedEventId={null}
        onSelectEvent={vi.fn()}
      />,
    )
    expect(screen.queryByText('Errors in range')).toBeNull()
  })

  it('renders category facets only for categories present in counts', () => {
    renderBody({
      data: response({
        events: [event()],
        coverage: [baseCoverage],
        categoryCounts: [{ category: 'run' as ActivityCategory, count: 11 }],
      }),
    })

    const runFacet = screen.getByRole('button', { name: 'run11' })
    expect(runFacet).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: /notification/i })).toBeNull()
  })

  it('links each source row to that source individual entries page', () => {
    renderBody({
      data: response({
        events: [event()],
        coverage: [baseCoverage],
      }),
    })

    const link = screen.getByRole('link', { name: 'agent-source' })
    expect(link).toHaveAttribute('href', '/logs/source-a')
  })

  it('renders coverage absent and error states honestly', () => {
    renderBody({
      data: response({
        events: [event()],
        coverage: [{ ...baseCoverage, status: 'absent', earliest: undefined }],
      }),
    })

    expect(screen.getByText('not recorded')).toBeInTheDocument()
    const firstCoverageRow = sourceRows()[0]
    const coverageCells = within(firstCoverageRow).getAllByRole('cell')
    expect(coverageCells[5]).toHaveTextContent('—')

    renderBody({
      data: response({
        events: [event()],
        coverage: [{ ...baseCoverage, id: 'source-b', status: 'error', error: 'coverage failed' }],
      }),
    })
    expect(screen.getByText('coverage failed')).toBeInTheDocument()
  })

  it('uses project → host → runtime → session for the Context column', () => {
    renderBody({
      grouping: 'off',
      data: response({
        coverage: [baseCoverage],
        events: [
          event({ id: 'event-missing', title: 'missing', project: undefined, host: undefined, runtime: undefined, session: undefined }),
          event({ id: 'event-host', title: 'host-only', host: 'buildbox-1' }),
          event({ id: 'event-runtime', title: 'runtime-only', project: undefined, host: undefined, runtime: 'runtime-a', session: undefined }),
          event({ id: 'event-session', title: 'session-only', project: undefined, host: undefined, runtime: undefined, session: 'session-a' }),
        ],
      }),
    })

    const rows = within(screen.getByRole('table', { name: 'Activity events' }))
      .getAllByRole('row')
      .filter((row) => row.querySelector('[data-event-row]'))

    const contextFor = (id: string): string => {
      const row = rows.find((entry) => entry.querySelector(`[data-event-row="event:${id}"]`))
      const cells = within(row!).getAllByRole('cell')
      return cells[6]!.textContent ?? ''
    }

    expect(contextFor('event-missing')).toBe('—')
    expect(contextFor('event-host')).toBe('buildbox-1')
    expect(contextFor('event-runtime')).toBe('runtime-a')
    expect(contextFor('event-session')).toBe('session-a')
  })

  it('shows the account in the row and the detail, and an em dash when absent', () => {
    renderBody({
      grouping: 'off',
      data: response({
        coverage: [baseCoverage],
        events: [
          event({ id: 'event-account', title: 'with-account', account: 'zync2' }),
          event({ id: 'event-no-account', title: 'without-account' }),
        ],
      }),
      selectedEventId: 'event:event-account',
    })

    const table = within(screen.getByRole('table', { name: 'Activity events' }))
    expect(within(table.getByRole('columnheader', { name: /^Account/ })).getByRole('button', { name: /^Account/ })).toBeInTheDocument()

    const rows = table.getAllByRole('row').filter((row) => row.querySelector('[data-event-row]'))
    const accountFor = (id: string): string => {
      const row = rows.find((entry) => entry.querySelector(`[data-event-row="event:${id}"]`))
      return within(row!).getAllByRole('cell')[5]!.textContent ?? ''
    }

    expect(accountFor('event-account')).toBe('zync2')
    expect(accountFor('event-no-account')).toBe('—')

    const detail = screen.getByRole('dialog')
    expect(within(detail).getByText('account')).toBeInTheDocument()
    expect(within(detail).getByText('zync2')).toBeInTheDocument()
  })

  it('passes D1 by re-rendering an absent time into a present time without throwing', () => {
    const result = renderBody({
      data: response({
        events: [event({ id: 'event-absent', ts: '' })],
        coverage: [baseCoverage],
      }),
    })

    expect(() => {
      result.rerender(
        <LogsContentBody
          data={response({
            events: [event({ id: 'event-absent', ts: '2026-08-08T10:00:00.000Z' })],
            coverage: [baseCoverage],
          })}
          errorTotal={undefined}
          range="1d"
          category={undefined}
          project={undefined}
          severityFloor={undefined}
          grouping="on"
          searchText=""
          onRange={vi.fn()}
          onCategory={vi.fn()}
          onProject={vi.fn()}
          onSeverity={vi.fn()}
          onGrouping={vi.fn()}
          onSearch={vi.fn()}
          selectedEventId={null}
          onSelectEvent={vi.fn()}
        />,
      )
    }).not.toThrow()
  })

  it('reports a group span as a forward duration, never a negative one', () => {
    renderBody({
      data: response({
        events: [
          event({ id: 'span-a', dedupeKey: 'span', ts: '2026-08-08T12:00:00.000Z', title: 'span newest' }),
          event({ id: 'span-b', dedupeKey: 'span', ts: '2026-08-08T11:00:00.000Z', title: 'span oldest' }),
        ],
        coverage: [baseCoverage],
      }),
      grouping: 'on',
    })

    const groupRow = screen.getByText('span newest').closest('tr')
    expect(groupRow).toHaveTextContent('span 1.0h')
    expect(groupRow?.textContent).not.toContain('-3600s')
  })

  it('groups by dedupeKey only, keeps no-dedupe events separate, and respects the toggle', () => {
    const grouped = {
      events: [
        event({ id: 'event-a1', dedupeKey: 'same', ts: '2026-08-08T12:00:00.000Z', title: 'grouped newest' }),
        event({ id: 'event-a2', dedupeKey: 'same', ts: '2026-08-08T11:00:00.000Z', title: 'grouped older' }),
        event({ id: 'event-b1', ts: '2026-08-08T10:00:00.000Z', title: 'single' }),
      ],
      coverage: [baseCoverage],
    }

    const result = renderBody({
      data: response(grouped),
      grouping: 'on',
    })

    expect(eventRowKeys()).toEqual(['group:same', 'event:event-b1'])
    const groupRow = screen.getByText('grouped newest').closest('tr')
    expect(groupRow).toHaveTextContent('×2')

    result.rerender(
      <LogsContentBody
        data={response(grouped)}
        errorTotal={undefined}
        range="1d"
        category={undefined}
        project={undefined}
        severityFloor={undefined}
        grouping="off"
        searchText=""
        onRange={vi.fn()}
        onCategory={vi.fn()}
        onProject={vi.fn()}
        onSeverity={vi.fn()}
        onGrouping={vi.fn()}
        onSearch={vi.fn()}
        selectedEventId={null}
        onSelectEvent={vi.fn()}
      />,
    )
    expect(eventRowKeys()).toEqual(['event:event-a1', 'event:event-a2', 'event:event-b1'])
  })

  it('sorts by Time and Severity using DataTable headers', () => {
    renderBody({
      data: response({
        events: [
          event({ id: 'event-newest', title: 'newest', ts: '2026-08-08T12:00:00.000Z', severity: 'warn' }),
          event({ id: 'event-middle', title: 'middle', ts: '2026-08-08T11:00:00.000Z', severity: 'error' }),
          event({ id: 'event-oldest', title: 'oldest', ts: '2026-08-08T10:00:00.000Z', severity: 'info' }),
        ],
        total: 3,
        coverage: [baseCoverage],
      }),
      grouping: 'off',
    })

    const table = screen.getByRole('table', { name: 'Activity events' })
    const timeHeader = within(table).getByRole('button', { name: /Time/i })
    const severityHeader = within(table).getByRole('button', { name: /Severity/i })

    expect(timeHeader.closest('th')).toHaveAttribute('aria-sort', 'descending')
    expect(eventRowKeys()).toEqual([
      'event:event-newest',
      'event:event-middle',
      'event:event-oldest',
    ])

    fireEvent.click(timeHeader)
    expect(timeHeader.closest('th')).toHaveAttribute('aria-sort', 'none')
    fireEvent.click(timeHeader)
    expect(timeHeader.closest('th')).toHaveAttribute('aria-sort', 'ascending')
    expect(eventRowKeys()).toEqual([
      'event:event-oldest',
      'event:event-middle',
      'event:event-newest',
    ])

    fireEvent.click(severityHeader)
    expect(severityHeader.closest('th')).toHaveAttribute('aria-sort', 'ascending')
    expect(eventRowKeys()).toEqual([
      'event:event-oldest',
      'event:event-newest',
      'event:event-middle',
    ])
    expect(ACTIVITY_SEVERITY_ORDER.info).toBeLessThan(ACTIVITY_SEVERITY_ORDER.warn)
    expect(ACTIVITY_SEVERITY_ORDER.warn).toBeLessThan(ACTIVITY_SEVERITY_ORDER.error)
  })

  it('renders event severity values through StatusChip with deck-ui category', () => {
    renderBody({
      data: response({
        events: [
          event({ id: 'event-sev-error', severity: 'error', title: 'error' }),
          event({ id: 'event-sev-notice', severity: 'notice', title: 'notice' }),
        ],
        total: 2,
        coverage: [baseCoverage],
      }),
      grouping: 'off',
    })

    const rows = within(screen.getByRole('table', { name: 'Activity events' }))
      .getAllByRole('row')
      .filter((row) => row.querySelector('[data-event-row^="event:"]'))

    expect(within(rows[0]!).getByTestId('status-chip')).toHaveAttribute('data-status-category', planStatusCategory('error'))
    expect(within(rows[1]!).getByTestId('status-chip')).toHaveAttribute('data-status-category', planStatusCategory('notice'))
  })

  it('opens a detail drawer with grouped and flattened event data', () => {
    renderBody({
      data: response({
        events: [
          event({
            id: 'event-detail',
            detail: {
              user: {
                name: 'alice',
              },
              tags: ['blue', 'green'],
              nested: {
                level: 2,
                nullish: null,
              },
            },
            project: 'project-x',
            rc: 1,
            durationMs: 42_000,
            title: 'detailed',
          }),
        ],
        total: 1,
        coverage: [baseCoverage],
      }),
      selectedEventId: null,
      onSelectEvent: vi.fn(),
    })
    fireEvent.click(screen.getByRole('button', { name: 'Details' }))
    const drawer = screen.getByRole('dialog')
    expect(drawer).toHaveTextContent('event-detail')
    expect(drawer).toHaveTextContent('project-x')
    expect(drawer).toHaveTextContent('detail.user.name')
    expect(drawer).toHaveTextContent('alice')
    expect(drawer).toHaveTextContent('detail.tags[0]')
    expect(drawer).toHaveTextContent('blue')
    expect(drawer).not.toHaveTextContent('null')
  })

  it('shows truncation notice only when data is truncated', () => {
    const result = renderBody({
      data: response({
        events: [event(), event({ id: 'event-2' }), event({ id: 'event-3' })],
        total: 10,
        truncated: true,
        coverage: [baseCoverage],
      }),
    })

    expect(screen.getByText(/Showing the newest 3 of 10 events in this range/)).toBeInTheDocument()

    result.rerender(
      <LogsContentBody
        data={response({
          events: [event(), event({ id: 'event-2' }), event({ id: 'event-3' })],
          total: 10,
          truncated: false,
          coverage: [baseCoverage],
        })}
        errorTotal={undefined}
        range="1d"
        category={undefined}
        project={undefined}
        severityFloor={undefined}
        grouping="on"
        searchText=""
        onRange={vi.fn()}
        onCategory={vi.fn()}
        onProject={vi.fn()}
        onSeverity={vi.fn()}
        onGrouping={vi.fn()}
        onSearch={vi.fn()}
        selectedEventId={null}
        onSelectEvent={vi.fn()}
      />,
    )
    expect(screen.queryByText(/Showing the newest 3 of 10 events in this range/)).toBeNull()
  })

  it('renders the suppressed-notifications table and its drill-down caption', () => {
    renderBody({
      data: response({
        events: [event()],
        coverage: [baseCoverage],
        suppressedNotifications: {
          status: 'ok',
          path: '/var/overdeck/notifications',
          rows: [
            { id: 's-1', program: 'agent', summary: 'notice', count: 3, last: '2026-08-08T10:00:00.000Z' },
            { id: 's-2', program: 'runner', summary: 'notice', count: 7, last: '2026-08-08T10:30:00.000Z' },
          ],
          sourceCount: 2,
          attemptCount: 10,
          skipped: 0,
        },
      }),
    })

    expect(screen.getByText(/Every individual attempt, with its full text, is in the/)).toBeInTheDocument()
    expect(screen.getByRole('link', { name: 'notification gate source' })).toBeInTheDocument()
    const rows = suppressedTableRows()
    expect(rows).toHaveLength(2)
    expect(rows[0]).toHaveTextContent('agent')
    expect(rows[0]).toHaveTextContent('3')
    expect(rows[1]).toHaveTextContent('runner')
    expect(rows[1]).toHaveTextContent('7')
  })

  it('renders zero-event and zero-suppressed copy instead of blank tables', () => {
    renderBody({
      data: response({
        events: [],
        total: 0,
        coverage: [baseCoverage],
        suppressedNotifications: {
          status: 'ok',
          path: '/var/overdeck/notifications',
          rows: [],
          sourceCount: 0,
          attemptCount: 0,
          skipped: 0,
        },
      }),
    })

    expect(screen.getByText('No activity matched these filters in this range.')).toBeInTheDocument()
    expect(screen.queryByRole('table', { name: 'Activity events' })).toBeNull()
    expect(screen.getByText('No suppressed notifications matched these filters in this range.')).toBeInTheDocument()
    expect(screen.queryByRole('table', { name: 'Suppressed notifications' })).toBeNull()
  })

  it('keeps source coverage absent/error visible and does not fabricate values', () => {
    renderBody({
      data: response({
        events: [event({ id: 'event-missing-fields' })],
        coverage: [{
          ...baseCoverage,
          status: 'absent',
        }],
        suppressedNotifications: {
          status: 'absent',
          path: '/var/overdeck/notifications',
          rows: [
            { id: 's-1', program: 'agent', summary: 'summary', count: 1, last: '2026-08-08T10:00:00.000Z' },
          ],
          sourceCount: 0,
          attemptCount: 0,
          skipped: 0,
        },
      }),
    })

    expect(screen.getAllByText('not recorded').length).toBeGreaterThan(0)

    renderBody({
      data: response({
        events: [event()],
        coverage: [{ ...baseCoverage, status: 'error', error: 'source failed' }],
        suppressedNotifications: {
          status: 'error',
          path: '/var/overdeck/notifications',
          rows: [],
          sourceCount: 0,
          attemptCount: 0,
          skipped: 0,
          error: 'suppressed query failed',
        },
      }),
    })

    expect(screen.getByText('source failed')).toBeInTheDocument()
    expect(screen.getByText('suppressed query failed')).toBeInTheDocument()
  })

  it('omits undefined event fields from the details panel and context fallback shows a dash', () => {
    renderBody({
      data: response({
        events: [
          event({
            id: 'event-blank',
            project: undefined,
            host: undefined,
            runtime: undefined,
            session: undefined,
            rc: undefined,
            durationMs: undefined,
            detail: undefined,
          }),
        ],
        coverage: [baseCoverage],
        total: 1,
      }),
      grouping: 'off',
      selectedEventId: 'event:event-blank',
    })

    const row = screen.getAllByRole('row').find((entry) => entry.querySelector('[data-event-row="event:event-blank"]'))
    const cells = within(row!).getAllByRole('cell')
    expect(cells[5]).toHaveTextContent('—')

    expect(screen.getByRole('dialog')).not.toHaveTextContent('undefined')
  })
})
