// @vitest-environment jsdom
/**
 * test/digest.test.ts (W3 quick-wins c) — reconnect digest banner (public/digest.ts).
 *
 * Pure helpers (normalizeDigest / digestSummary / renderDigestBanner) + the
 * mountDigest wiring with a mocked fetch: fetches with the stored last-seen,
 * shows a banner only when something happened, dismiss advances last-seen + hides,
 * and a fetch failure shows no banner (best-effort). localStorage comes from the
 * shared jsdom polyfill (vitest.config setupFiles).
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import type { DigestResult } from '../src/types.js'
import {
  normalizeDigest,
  digestSummary,
  digestHighlightCount,
  renderDigestBanner,
  getLastSeen,
  setLastSeen,
  mountDigest,
} from '../public/digest.js'

function makeDigest(over: Partial<DigestResult> = {}): DigestResult {
  return {
    since: 0,
    generatedAt: 5000,
    total: 0,
    finished: 0,
    needsInput: 0,
    stuck: 0,
    working: 0,
    totalCostUsd: 0,
    sessions: [],
    ...over,
  }
}

/** Install a fetch mock returning `body` (or rejecting when `body` is null). */
function mockFetch(body: unknown, ok = true): ReturnType<typeof vi.fn> {
  const fn = vi.fn(async () => {
    if (body === null) throw new Error('network down')
    return { ok, json: async () => body } as Response
  })
  vi.stubGlobal('fetch', fn)
  return fn
}

beforeEach(() => {
  localStorage.clear()
  document.body.innerHTML = ''
})

afterEach(() => {
  vi.unstubAllGlobals()
})

// ── pure helpers ──────────────────────────────────────────────────────────────

describe('normalizeDigest', () => {
  it('returns null for a non-object / missing generatedAt', () => {
    expect(normalizeDigest(null)).toBeNull()
    expect(normalizeDigest('x')).toBeNull()
    expect(normalizeDigest({ total: 1 })).toBeNull()
  })

  it('coerces a well-formed response', () => {
    const d = normalizeDigest(makeDigest({ finished: 2, totalCostUsd: 1.5 }))
    expect(d?.finished).toBe(2)
    expect(d?.totalCostUsd).toBe(1.5)
    expect(Array.isArray(d?.sessions)).toBe(true)
  })

  it('defaults missing numeric counts to 0 and non-array sessions to []', () => {
    const d = normalizeDigest({ generatedAt: 10 })
    expect(d?.finished).toBe(0)
    expect(d?.sessions).toEqual([])
  })
})

describe('digestSummary / digestHighlightCount', () => {
  it('counts finished + needsInput + stuck', () => {
    expect(digestHighlightCount(makeDigest({ finished: 2, needsInput: 1, stuck: 1, working: 9 }))).toBe(4)
  })

  it('builds a compact summary of only the non-zero buckets', () => {
    const s = digestSummary(makeDigest({ finished: 2, stuck: 1 }))
    expect(s).toContain('2 finished')
    expect(s).toContain('1 stuck')
    expect(s).not.toContain('waiting')
  })
})

describe('renderDigestBanner', () => {
  it('returns null when nothing worth showing', () => {
    expect(renderDigestBanner(makeDigest({ working: 3 }), () => {})).toBeNull()
  })

  it('renders a dismissible banner and wires the × button', () => {
    const onDismiss = vi.fn()
    const banner = renderDigestBanner(makeDigest({ finished: 1 }), onDismiss)
    expect(banner?.className).toContain('wya-banner')
    ;(banner?.querySelector('.wya-dismiss') as HTMLButtonElement).click()
    expect(onDismiss).toHaveBeenCalled()
  })

  it('renders an attacker-influenced summary as inert text (SEC-H5)', () => {
    const banner = renderDigestBanner(makeDigest({ finished: 1 }), () => {})
    expect(banner?.querySelectorAll('script').length).toBe(0)
  })
})

describe('getLastSeen / setLastSeen', () => {
  it('defaults to 0 and round-trips a value', () => {
    expect(getLastSeen()).toBe(0)
    setLastSeen(1234)
    expect(getLastSeen()).toBe(1234)
  })
})

// ── mountDigest (wiring) ──────────────────────────────────────────────────────

describe('mountDigest', () => {
  it('fetches with the stored last-seen watermark', async () => {
    setLastSeen(500)
    const fetchFn = mockFetch(makeDigest({ finished: 1 }))
    await mountDigest(document.body)
    expect(fetchFn).toHaveBeenCalledWith('/digest?since=500')
  })

  it('shows a banner when something happened', async () => {
    mockFetch(makeDigest({ finished: 1 }))
    await mountDigest(document.body)
    expect(document.body.querySelector('.wya-banner')).not.toBeNull()
  })

  it('shows NO banner when nothing happened', async () => {
    mockFetch(makeDigest({ working: 2 }))
    await mountDigest(document.body)
    expect(document.body.querySelector('.wya-banner')).toBeNull()
  })

  it('advances last-seen to generatedAt after a fetch', async () => {
    mockFetch(makeDigest({ generatedAt: 8888, finished: 1 }))
    await mountDigest(document.body)
    expect(getLastSeen()).toBe(8888)
  })

  it('dismiss updates last-seen and removes the banner', async () => {
    mockFetch(makeDigest({ generatedAt: 7777, needsInput: 1 }))
    await mountDigest(document.body)
    const banner = document.body.querySelector('.wya-banner')
    expect(banner).not.toBeNull()
    ;(banner!.querySelector('.wya-dismiss') as HTMLButtonElement).click()
    expect(document.body.querySelector('.wya-banner')).toBeNull()
    expect(getLastSeen()).toBe(7777)
  })

  it('shows no banner on a fetch failure (best-effort)', async () => {
    mockFetch(null)
    const banner = await mountDigest(document.body)
    expect(banner).toBeNull()
    expect(document.body.querySelector('.wya-banner')).toBeNull()
  })
})
