import { test, expect, describe } from "bun:test"
import {
  RateLimitResumeScheduler,
  type ResumeSchedulerChat,
} from "./resume-scheduler"

const MAX_TIMER_MS = 2_147_483_647
const flush = () => new Promise<void>((resolve) => setTimeout(resolve, 0))

interface FakeTimer {
  fn: () => void
  delayMs: number
  cancelled: boolean
}

function makeHarness(chatOverrides: Partial<ResumeSchedulerChat> = {}) {
  const timers: FakeTimer[] = []
  const schedule = (fn: () => void, delayMs: number) => {
    const entry: FakeTimer = { fn, delayMs, cancelled: false }
    timers.push(entry)
    return () => { entry.cancelled = true }
  }

  const chat: ResumeSchedulerChat = {
    id: "c1",
    provider: "claude",
    model: "opus",
    planMode: false,
    autoContinueOnRateLimit: true,
    ...chatOverrides,
  }

  let queued: unknown = null
  const active = new Set<string>()
  const startCalls: Array<Record<string, unknown>> = []

  const store = {
    requireChat: (id: string): ResumeSchedulerChat => {
      if (id !== chat.id) throw new Error(`no chat ${id}`)
      return chat
    },
    getQueuedTurn: (_id: string) => queued,
  }

  const coordinator = {
    activeTurns: { has: (id: string) => active.has(id) },
    startTurnForChat: async (args: Record<string, unknown>) => { startCalls.push(args) },
  }

  let nowMs = 1_000_000
  const scheduler = new RateLimitResumeScheduler({
    store,
    coordinator,
    now: () => nowMs,
    schedule,
  })

  return {
    scheduler,
    timers,
    startCalls,
    chat,
    setQueued: (q: unknown) => { queued = q },
    setActive: (on: boolean) => { on ? active.add(chat.id) : active.delete(chat.id) },
    setNow: (n: number) => { nowMs = n },
    fireFirst: async () => { timers[timers.length - 1].fn(); await flush() },
  }
}

describe("RateLimitResumeScheduler.onRateLimit", () => {
  test("arms a timer at resetAt - now when the chat opted in", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000) // now=1_000_000
    expect(h.timers).toHaveLength(1)
    expect(h.timers[0].delayMs).toBe(5_000)
    expect(h.scheduler.hasPending("c1")).toBe(true)
  })

  test("no-op when the chat did not opt in", () => {
    const h = makeHarness({ autoContinueOnRateLimit: false })
    h.scheduler.onRateLimit("c1", 1_005_000)
    expect(h.timers).toHaveLength(0)
    expect(h.scheduler.hasPending("c1")).toBe(false)
  })

  test("no-op for a deleted chat", () => {
    const h = makeHarness({ deletedAt: 123 })
    h.scheduler.onRateLimit("c1", 1_005_000)
    expect(h.timers).toHaveLength(0)
  })

  test("no-op for an unknown chat", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("missing", 1_005_000)
    expect(h.timers).toHaveLength(0)
  })

  test("clamps a past resetAt to 0 and a far-future one to the 32-bit ceiling", () => {
    const past = makeHarness()
    past.scheduler.onRateLimit("c1", 500_000) // before now
    expect(past.timers[0].delayMs).toBe(0)

    const far = makeHarness()
    far.scheduler.onRateLimit("c1", Number.MAX_SAFE_INTEGER)
    expect(far.timers[0].delayMs).toBe(MAX_TIMER_MS)
  })

  test("ignores a non-finite resetAt", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", Number.NaN)
    expect(h.timers).toHaveLength(0)
  })

  test("re-arm replaces the prior timer", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.scheduler.onRateLimit("c1", 1_010_000)
    expect(h.timers).toHaveLength(2)
    expect(h.timers[0].cancelled).toBe(true)
    expect(h.timers[1].cancelled).toBe(false)
    expect(h.timers[1].delayMs).toBe(10_000)
  })
})

describe("RateLimitResumeScheduler fire", () => {
  test("resumes via startTurnForChat with a Continue. nudge and appendUserPrompt:false", async () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    await h.fireFirst()
    expect(h.startCalls).toHaveLength(1)
    expect(h.startCalls[0]).toMatchObject({
      chatId: "c1",
      provider: "claude",
      content: "Continue.",
      model: "opus",
      planMode: false,
      appendUserPrompt: false,
    })
    expect(h.scheduler.hasPending("c1")).toBe(false)
  })

  test("falls back to claude/sonnet when provider/model are unset", async () => {
    const h = makeHarness({ provider: null, model: null })
    h.scheduler.onRateLimit("c1", 1_005_000)
    await h.fireFirst()
    expect(h.startCalls[0]).toMatchObject({ provider: "claude", model: "sonnet" })
  })

  test("no-op when a turn is already active", async () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.setActive(true)
    await h.fireFirst()
    expect(h.startCalls).toHaveLength(0)
  })

  test("no-op when a queued turn exists", async () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.setQueued({ chatId: "c1", content: "x" })
    await h.fireFirst()
    expect(h.startCalls).toHaveLength(0)
  })

  test("no-op when the chat was disarmed during the wait", async () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.chat.autoContinueOnRateLimit = false
    await h.fireFirst()
    expect(h.startCalls).toHaveLength(0)
  })

  test("no-op when the chat was deleted during the wait", async () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.chat.deletedAt = Date.now()
    await h.fireFirst()
    expect(h.startCalls).toHaveLength(0)
  })
})

describe("RateLimitResumeScheduler.cancel", () => {
  test("cancel clears a pending timer", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.scheduler.cancel("c1")
    expect(h.timers[0].cancelled).toBe(true)
    expect(h.scheduler.hasPending("c1")).toBe(false)
  })

  test("cancelAll clears every pending timer", () => {
    const h = makeHarness()
    h.scheduler.onRateLimit("c1", 1_005_000)
    h.scheduler.cancelAll()
    expect(h.scheduler.hasPending("c1")).toBe(false)
  })
})
