/**
 * Tests for RunMonitor slot props + streamSource.
 *
 * The composite is intentionally rendered with `streamSource='none'` so the
 * tests don't try to open an EventSource against a real URL — that path is
 * exercised by the e2e suite. The default `'auto'` path is asserted via
 * data attributes and the live-badge presence.
 */

import { describe, it, expect, vi } from 'vitest'
import * as React from 'react'
import { render, screen } from '@testing-library/react'

import { RunMonitor, type RunMonitorProps } from '../run-monitor'
import type { LiveLogTailHandle } from '../../primitives/live-log-tail'

const baseProps: Pick<
  RunMonitorProps,
  'runId' | 'streamUrl' | 'phases' | 'currentPhase' | 'status'
> = {
  runId: 42,
  streamUrl: '/api/runs/42/stream',
  phases: [
    { key: 'export', label: 'Export' },
    { key: 'train', label: 'Train' },
  ],
  currentPhase: 'train',
  status: 'running',
}

// Stub EventSource so the 'auto' streamSource code path doesn't reach out
// to the network in jsdom. Each test that needs auto mode replaces this.
class StubEventSource {
  url: string
  withCredentials: boolean
  readyState = 1
  onopen: (() => void) | null = null
  onerror: (() => void) | null = null
  constructor(url: string, init?: { withCredentials?: boolean }) {
    this.url = url
    this.withCredentials = init?.withCredentials ?? false
  }
  addEventListener(): void {}
  removeEventListener(): void {}
  close(): void {
    this.readyState = 2
  }
}

describe('RunMonitor', () => {
  it('renders the header slot at top-left', () => {
    render(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        header={<div data-testid="run-monitor-header">My Run</div>}
      />
    )
    expect(screen.getByTestId('run-monitor-header')).toBeTruthy()
  })

  it('renders the actions slot at top-right (no SSE badge in streamSource=none mode)', () => {
    render(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        actions={<button data-testid="cancel-btn">Cancel</button>}
      />
    )
    expect(screen.getByTestId('cancel-btn')).toBeTruthy()
    // SSE live badge is suppressed when streamSource='none' since the
    // composite is not managing an EventSource of its own.
    expect(screen.queryByText(/live|connecting/i)).toBeNull()
  })

  it('renders the summary slot after the per-case table', () => {
    render(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        summary={<div data-testid="run-summary">All cases passed</div>}
      />
    )
    expect(screen.getByTestId('run-summary')).toBeTruthy()
  })

  it('renders the error slot only when status is failed or cancelled', () => {
    const { rerender, container } = render(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        status="running"
        error={<div data-testid="run-error">Boom</div>}
      />
    )
    expect(screen.queryByTestId('run-error')).toBeNull()
    expect(container.querySelector('[data-slot="run-monitor-error"]')).toBeNull()

    rerender(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        status="failed"
        error={<div data-testid="run-error">Boom</div>}
      />
    )
    expect(screen.getByTestId('run-error')).toBeTruthy()
    const errorPanel = container.querySelector('[data-slot="run-monitor-error"]')
    expect(errorPanel).toBeTruthy()
    expect(errorPanel?.getAttribute('role')).toBe('alert')
  })

  it('exposes streamSource via a data attribute', () => {
    const { container } = render(<RunMonitor {...baseProps} streamSource="none" />)
    const root = container.querySelector('[data-slot="run-monitor"]')
    expect(root?.getAttribute('data-stream-source')).toBe('none')
  })

  it('preserves v0.3.0 onCancel/onResume back-compat when no actions slot is supplied', () => {
    const onCancel = vi.fn()
    const { container } = render(
      <RunMonitor {...baseProps} streamSource="none" status="running" onCancel={onCancel} />
    )
    // The Cancel back-compat button should render — find by visible text.
    const cancelBtn = Array.from(container.querySelectorAll('button')).find(
      btn => btn.textContent === 'Cancel'
    )
    expect(cancelBtn).toBeTruthy()
    cancelBtn?.click()
    expect(onCancel).toHaveBeenCalledTimes(1)
  })

  it('skips back-compat onCancel when an actions slot is provided', () => {
    const onCancel = vi.fn()
    const { container } = render(
      <RunMonitor
        {...baseProps}
        streamSource="none"
        status="running"
        onCancel={onCancel}
        actions={<button data-testid="my-cancel">My Cancel</button>}
      />
    )
    expect(container.querySelector('[data-testid="my-cancel"]')).toBeTruthy()
    const cancelBtn = Array.from(container.querySelectorAll('button')).find(
      btn => btn.textContent === 'Cancel'
    )
    expect(cancelBtn).toBeUndefined()
    expect(onCancel).not.toHaveBeenCalled()
  })

  it('forwards a logRef so callers can append lines externally', () => {
    const ref = React.createRef<LiveLogTailHandle>()
    render(<RunMonitor {...baseProps} streamSource="none" logRef={ref} />)
    expect(ref.current).not.toBeNull()
    expect(typeof ref.current?.append).toBe('function')
    expect(typeof ref.current?.clear).toBe('function')
  })

  it('does not leak sseToken to the DOM via data attributes', () => {
    const { container } = render(
      <RunMonitor {...baseProps} streamSource="none" sseToken="secret-token-value" />
    )
    expect(container.querySelector('[data-sse-token]')).toBeNull()
    const html = container.innerHTML
    expect(html).not.toContain('secret-token-value')
  })

  it('opens an EventSource when streamSource is auto (default)', () => {
    const original = globalThis.EventSource
    const created: string[] = []
    class TrackedSource extends StubEventSource {
      constructor(url: string, init?: { withCredentials?: boolean }) {
        super(url, init)
        created.push(url)
      }
    }
    globalThis.EventSource = TrackedSource as unknown as typeof EventSource

    try {
      render(<RunMonitor {...baseProps} streamUrl="/api/r/1" />)
      expect(created).toContain('/api/r/1')
    } finally {
      globalThis.EventSource = original
    }
  })
})
