// @vitest-environment jsdom
/**
 * test/telemetry-gauge.test.ts — renderTelemetryGauge + renderStatusBadge + statusText('stuck')
 *
 * Covers T-preview-grid W1: telemetry gauge, status badge, and statusText extension.
 * Security: SEC-H5 (textContent only), SEC-L5 (PR URL https scheme guard).
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { StatusTelemetry, ClaudeStatus } from '../src/types.js'

// Stub xterm so jsdom does not fail on canvas operations (same as preview-grid.test.ts)
vi.mock('@xterm/xterm', () => ({
  Terminal: class FakeTerm {
    cols = 80
    rows = 24
    open = vi.fn()
    reset = vi.fn()
    resize = vi.fn()
    dispose = vi.fn()
    write = vi.fn((_d: string, cb?: () => void) => cb?.())
  },
}))

const { renderTelemetryGauge, renderStatusBadge, statusText } = await import('../public/preview-grid.js')

/** Helper: build a StatusTelemetry with only `at` (everything else optional). */
function makeTelemetry(over: Partial<StatusTelemetry> = {}): StatusTelemetry {
  return { at: Date.now(), ...over }
}

function makeContainer(): HTMLDivElement {
  return document.createElement('div')
}

beforeEach(() => {
  vi.restoreAllMocks()
})

// ─────────────────────── statusText — stuck extension ────────────────────────

describe('statusText — stuck extension', () => {
  it('returns text containing ⚠ for stuck status', () => {
    const text = statusText('stuck' as ClaudeStatus)
    expect(text).toContain('⚠')
    expect(text).toContain('stuck')
  })

  it('still maps existing statuses correctly (no regression)', () => {
    expect(statusText('working')).toContain('working')
    expect(statusText('waiting')).toContain('waiting')
    expect(statusText('idle')).toContain('idle')
    expect(statusText('unknown')).toBe('·')
  })
})

// ─────────────────────── renderTelemetryGauge ────────────────────────────────

describe('renderTelemetryGauge', () => {
  it('renders nothing for null telemetry', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, null, 30_000)
    expect(c.children.length).toBe(0)
  })

  it('is tolerant when telemetry has only the required at field (no optional fields)', () => {
    const c = makeContainer()
    expect(() => renderTelemetryGauge(c, makeTelemetry(), 30_000)).not.toThrow()
    // No optional fields → nothing rendered beyond the stale logic
    expect(c.children.length).toBe(0)
  })

  it('clears existing children on each call', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ model: 'first' }), 30_000)
    renderTelemetryGauge(c, makeTelemetry({ model: 'second' }), 30_000)
    const chips = c.querySelectorAll('.tg-model')
    expect(chips.length).toBe(1)
    expect(chips[0]?.textContent).toBe('second')
  })

  // ── context bar ──

  it('renders a context bar when contextUsedPct is present', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 50 }), 30_000)
    expect(c.querySelector('.tg-ctx-bar')).not.toBeNull()
    expect(c.querySelector('.tg-ctx-fill')).not.toBeNull()
  })

  it('adds warning class on context fill when contextUsedPct > 80', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 85 }), 30_000)
    expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(true)
  })

  it('does NOT add warning class when contextUsedPct === 80', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 80 }), 30_000)
    expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(false)
  })

  it('does NOT add warning class when contextUsedPct < 80', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 60 }), 30_000)
    expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(false)
  })

  it('skips context bar when contextUsedPct is absent', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({}), 30_000)
    expect(c.querySelector('.tg-ctx-bar')).toBeNull()
  })

  // ── cost chip ──

  it('renders cost chip with $ prefix when costUsd is present', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ costUsd: 0.1234 }), 30_000)
    const cost = c.querySelector('.tg-cost')
    expect(cost).not.toBeNull()
    expect(cost?.textContent).toContain('$')
    expect(cost?.textContent).toContain('0.1234')
  })

  it('skips cost chip when costUsd is absent', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry(), 30_000)
    expect(c.querySelector('.tg-cost')).toBeNull()
  })

  // ── cost budget warn (W3 b) ──

  it('adds tg-cost-warn when costUsd >= costBudgetUsd', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ costUsd: 6 }), 30_000, 5)
    expect(c.querySelector('.tg-cost')?.classList.contains('tg-cost-warn')).toBe(true)
  })

  it('adds tg-cost-warn exactly at the budget boundary (>=)', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ costUsd: 5 }), 30_000, 5)
    expect(c.querySelector('.tg-cost')?.classList.contains('tg-cost-warn')).toBe(true)
  })

  it('does NOT add tg-cost-warn when costUsd < budget', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ costUsd: 3 }), 30_000, 5)
    expect(c.querySelector('.tg-cost')?.classList.contains('tg-cost-warn')).toBe(false)
  })

  it('does NOT add tg-cost-warn when budget is 0 or undefined', () => {
    const c1 = makeContainer()
    renderTelemetryGauge(c1, makeTelemetry({ costUsd: 6 }), 30_000, 0)
    expect(c1.querySelector('.tg-cost')?.classList.contains('tg-cost-warn')).toBe(false)

    const c2 = makeContainer()
    renderTelemetryGauge(c2, makeTelemetry({ costUsd: 6 }), 30_000)
    expect(c2.querySelector('.tg-cost')?.classList.contains('tg-cost-warn')).toBe(false)
  })

  // ── model chip ──

  it('renders model chip when model is present', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ model: 'claude-3-sonnet' }), 30_000)
    expect(c.querySelector('.tg-model')?.textContent).toBe('claude-3-sonnet')
  })

  it('renders <script> model name as plain text — not injected into DOM (SEC-H5)', () => {
    const c = makeContainer()
    const xss = '<script>alert(1)</script>'
    renderTelemetryGauge(c, makeTelemetry({ model: xss }), 30_000)
    const chip = c.querySelector('.tg-model')
    expect(chip?.textContent).toBe(xss)
    // No actual <script> element should exist inside the container
    expect(c.querySelectorAll('script').length).toBe(0)
  })

  it('skips model chip when model is absent', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry(), 30_000)
    expect(c.querySelector('.tg-model')).toBeNull()
  })

  // ── PR badge ──

  it('renders PR badge with link text for a PR', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 42, url: 'https://github.com/user/repo/pull/42' } }),
      30_000,
    )
    const link = c.querySelector('.tg-pr-link') as HTMLAnchorElement | null
    expect(link).not.toBeNull()
    expect(link?.textContent).toContain('42')
  })

  it('sets href for https PR URL (SEC-L5)', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 42, url: 'https://github.com/user/repo/pull/42' } }),
      30_000,
    )
    const link = c.querySelector('.tg-pr-link') as HTMLAnchorElement | null
    expect(link?.href).toBe('https://github.com/user/repo/pull/42')
  })

  it('does NOT set href for http PR URL (SEC-L5)', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 99, url: 'http://insecure.example.com/pull/99' } }),
      30_000,
    )
    const link = c.querySelector('.tg-pr-link') as HTMLAnchorElement | null
    expect(link?.textContent).toContain('99')
    // href attribute should not be set (attribute absent or empty string)
    expect(link?.getAttribute('href') ?? '').toBe('')
  })

  it('does NOT set href for an invalid PR URL (SEC-L5)', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 1, url: 'not-a-valid-url' } }),
      30_000,
    )
    const link = c.querySelector('.tg-pr-link') as HTMLAnchorElement | null
    expect(link?.getAttribute('href') ?? '').toBe('')
  })

  it('renders PR reviewState as text when present', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 5, url: 'https://example.com/pull/5', reviewState: 'APPROVED' } }),
      30_000,
    )
    expect(c.querySelector('.tg-pr-state')?.textContent).toBe('APPROVED')
  })

  it('skips PR reviewState element when reviewState is absent', () => {
    const c = makeContainer()
    renderTelemetryGauge(
      c,
      makeTelemetry({ pr: { number: 5, url: 'https://example.com/pull/5' } }),
      30_000,
    )
    expect(c.querySelector('.tg-pr-state')).toBeNull()
  })

  it('skips PR badge when pr is absent', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry(), 30_000)
    expect(c.querySelector('.tg-pr')).toBeNull()
  })

  // ── stale detection ──

  it('adds tg-stale class when telemetry age exceeds staleTtlMs', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ at: Date.now() - 60_000 }), 30_000)
    expect(c.classList.contains('tg-stale')).toBe(true)
  })

  it('does NOT add tg-stale class for fresh telemetry', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ at: Date.now() }), 30_000)
    expect(c.classList.contains('tg-stale')).toBe(false)
  })

  it('removes tg-stale class when subsequent call has fresh telemetry', () => {
    const c = makeContainer()
    // First: stale
    renderTelemetryGauge(c, makeTelemetry({ at: Date.now() - 60_000 }), 30_000)
    expect(c.classList.contains('tg-stale')).toBe(true)
    // Second: fresh
    renderTelemetryGauge(c, makeTelemetry({ at: Date.now() }), 30_000)
    expect(c.classList.contains('tg-stale')).toBe(false)
  })

  it('removes tg-stale class when called with null telemetry after stale', () => {
    const c = makeContainer()
    renderTelemetryGauge(c, makeTelemetry({ at: Date.now() - 60_000 }), 30_000)
    expect(c.classList.contains('tg-stale')).toBe(true)
    renderTelemetryGauge(c, null, 30_000)
    expect(c.classList.contains('tg-stale')).toBe(false)
  })
})

// ─────────────────────── renderStatusBadge ───────────────────────────────────

describe('renderStatusBadge', () => {
  it('renders a badge for stuck with ⚠ (A5)', () => {
    const c = makeContainer()
    renderStatusBadge(c, 'stuck')
    const badge = c.querySelector('.sb-badge')
    expect(badge).not.toBeNull()
    expect(badge?.textContent).toContain('⚠')
    expect(badge?.textContent).toContain('stuck')
  })

  it('applies per-status CSS class for styling', () => {
    const statuses: ClaudeStatus[] = ['working', 'waiting', 'idle', 'unknown', 'stuck']
    for (const status of statuses) {
      const c = makeContainer()
      renderStatusBadge(c, status)
      expect(c.querySelector(`.sb-${status}`)).not.toBeNull()
    }
  })

  it('clears the container before rendering (replaces old badge)', () => {
    const c = makeContainer()
    renderStatusBadge(c, 'working')
    renderStatusBadge(c, 'idle')
    expect(c.querySelectorAll('.sb-badge').length).toBe(1)
    expect(c.querySelector('.sb-badge')?.textContent).toContain('idle')
  })

  it('renders each non-stuck status with appropriate text', () => {
    const cases: Array<[ClaudeStatus, string]> = [
      ['working', 'working'],
      ['waiting', 'waiting'],
      ['idle', 'idle'],
    ]
    for (const [status, expected] of cases) {
      const c = makeContainer()
      renderStatusBadge(c, status)
      expect(c.querySelector('.sb-badge')?.textContent).toContain(expected)
    }
  })
})
