/**
 * test/voice-confirm.test.ts — Unit tests for voice-confirm.ts (VC / V1b).
 *
 * Pure timing-logic controller for the approve confirm window (PLAN_VOICE_COMMANDS §4,
 * decision B). Uses vi.useFakeTimers — no DOM, no WS; setTimeout/clearTimeout only.
 *
 * Covered behaviours:
 *  - arm(onHeard, onCommit) fires onHeard synchronously/immediately.
 *  - onCommit fires once the windowMs timer elapses.
 *  - cancel() before the window elapses → onCommit is NEVER called.
 *  - re-arming clears the prior timer — only the latest arm's onCommit fires.
 *  - isArmed() reflects true while pending, false after commit/cancel/before-arm.
 *  - default windowMs is 1500 when not provided.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { createApproveConfirm } from '../public/voice-confirm.js'

describe('voice-confirm', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

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

  it('fires onHeard immediately when armed', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    const onHeard = vi.fn()
    const onCommit = vi.fn()

    // Act
    confirm.arm(onHeard, onCommit)

    // Assert
    expect(onHeard).toHaveBeenCalledTimes(1)
    expect(onCommit).not.toHaveBeenCalled()
  })

  it('fires onCommit after windowMs elapses', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    const onHeard = vi.fn()
    const onCommit = vi.fn()

    // Act
    confirm.arm(onHeard, onCommit)
    vi.advanceTimersByTime(1499)
    expect(onCommit).not.toHaveBeenCalled()
    vi.advanceTimersByTime(1)

    // Assert
    expect(onCommit).toHaveBeenCalledTimes(1)
  })

  it('never calls onCommit when cancel() runs before the window elapses', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    const onHeard = vi.fn()
    const onCommit = vi.fn()
    confirm.arm(onHeard, onCommit)

    // Act
    vi.advanceTimersByTime(500)
    confirm.cancel()
    vi.advanceTimersByTime(10_000)

    // Assert
    expect(onCommit).not.toHaveBeenCalled()
  })

  it('cancel() before any arm() is a no-op that does not throw', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)

    // Act / Assert
    expect(() => confirm.cancel()).not.toThrow()
  })

  it('re-arming clears the prior timer so only the latest arm commits', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    const firstHeard = vi.fn()
    const firstCommit = vi.fn()
    const secondHeard = vi.fn()
    const secondCommit = vi.fn()

    // Act
    confirm.arm(firstHeard, firstCommit)
    vi.advanceTimersByTime(1000)
    confirm.arm(secondHeard, secondCommit)
    // Enough time for the FIRST window to have elapsed (1000 + 1000 = 2000 > 1500)
    // but not enough for the second window (armed at t=1000, needs t=2500).
    vi.advanceTimersByTime(1000)

    // Assert: first window's commit never fires, second hasn't fired yet either.
    expect(firstCommit).not.toHaveBeenCalled()
    expect(secondCommit).not.toHaveBeenCalled()
    expect(secondHeard).toHaveBeenCalledTimes(1)

    // Advance past the second window's own deadline.
    vi.advanceTimersByTime(500)
    expect(secondCommit).toHaveBeenCalledTimes(1)
    expect(firstCommit).not.toHaveBeenCalled()
  })

  it('isArmed() is false before any arm()', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)

    // Assert
    expect(confirm.isArmed()).toBe(false)
  })

  it('isArmed() is true immediately after arm() and while the window is pending', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)

    // Act
    confirm.arm(vi.fn(), vi.fn())

    // Assert
    expect(confirm.isArmed()).toBe(true)
    vi.advanceTimersByTime(500)
    expect(confirm.isArmed()).toBe(true)
  })

  it('isArmed() is false after onCommit fires', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    confirm.arm(vi.fn(), vi.fn())

    // Act
    vi.advanceTimersByTime(1500)

    // Assert
    expect(confirm.isArmed()).toBe(false)
  })

  it('isArmed() is false after cancel()', () => {
    // Arrange
    const confirm = createApproveConfirm(1500)
    confirm.arm(vi.fn(), vi.fn())

    // Act
    confirm.cancel()

    // Assert
    expect(confirm.isArmed()).toBe(false)
  })

  it('uses a default windowMs of 1500 when not provided', () => {
    // Arrange
    const confirm = createApproveConfirm()
    const onCommit = vi.fn()

    // Act
    confirm.arm(vi.fn(), onCommit)
    vi.advanceTimersByTime(1499)
    expect(onCommit).not.toHaveBeenCalled()
    vi.advanceTimersByTime(1)

    // Assert
    expect(onCommit).toHaveBeenCalledTimes(1)
  })
})
