import { afterEach, beforeEach, describe, expect, test } from "bun:test"

import type { BackgroundManager } from "../features/background-agent"
import { setMainSession, subagentSessions } from "../features/claude-code-session-state"
import { createTodoContinuationEnforcer } from "./todo-continuation-enforcer"

describe("todo-continuation-enforcer", () => {
  let promptCalls: Array<{ sessionID: string; agent?: string; model?: { providerID?: string; modelID?: string }; text: string }>
  let toastCalls: Array<{ title: string; message: string }>

  interface MockMessage {
    info: {
      id: string
      role: "user" | "assistant"
      error?: { name: string; data?: { message: string } }
    }
  }

  let mockMessages: MockMessage[] = []

  function createMockPluginInput() {
    return {
      client: {
        session: {
          todo: async () => ({ data: [
            { id: "1", content: "Task 1", status: "pending", priority: "high" },
            { id: "2", content: "Task 2", status: "completed", priority: "medium" },
          ]}),
          messages: async () => ({ data: mockMessages }),
          prompt: async (opts: any) => {
            promptCalls.push({
              sessionID: opts.path.id,
              agent: opts.body.agent,
              model: opts.body.model,
              text: opts.body.parts[0].text,
            })
            return {}
          },
        },
        tui: {
          showToast: async (opts: any) => {
            toastCalls.push({
              title: opts.body.title,
              message: opts.body.message,
            })
            return {}
          },
        },
      },
      directory: "/tmp/test",
    } as any
  }

  function createMockBackgroundManager(runningTasks: boolean = false): BackgroundManager {
    return {
      getTasksByParentSession: () => runningTasks
        ? [{ status: "running" }]
        : [],
    } as any
  }

  beforeEach(() => {
    promptCalls = []
    toastCalls = []
    mockMessages = []
    setMainSession(undefined)
    subagentSessions.clear()
  })

  afterEach(() => {
    setMainSession(undefined)
    subagentSessions.clear()
  })

  test("should inject continuation when idle with incomplete todos", async () => {
    // #given - main session with incomplete todos
    const sessionID = "main-123"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
      backgroundManager: createMockBackgroundManager(false),
    })

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #then - countdown toast shown
    await new Promise(r => setTimeout(r, 100))
    expect(toastCalls.length).toBeGreaterThanOrEqual(1)
    expect(toastCalls[0].title).toBe("Todo Continuation")

    // #then - after countdown, continuation injected
    await new Promise(r => setTimeout(r, 2500))
    expect(promptCalls.length).toBe(1)
    expect(promptCalls[0].text).toContain("TODO CONTINUATION")
  })

  test("should not inject when all todos are complete", async () => {
    // #given - session with all todos complete
    const sessionID = "main-456"
    setMainSession(sessionID)

    const mockInput = createMockPluginInput()
    mockInput.client.session.todo = async () => ({ data: [
      { id: "1", content: "Task 1", status: "completed", priority: "high" },
    ]})

    const hook = createTodoContinuationEnforcer(mockInput, {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected
    expect(promptCalls).toHaveLength(0)
  })

  test("should not inject when background tasks are running", async () => {
    // #given - session with running background tasks
    const sessionID = "main-789"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
      backgroundManager: createMockBackgroundManager(true),
    })

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected
    expect(promptCalls).toHaveLength(0)
  })

  test("should not inject for non-main session", async () => {
    // #given - main session set, different session goes idle
    setMainSession("main-session")
    const otherSession = "other-session"

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - non-main session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID: otherSession } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected
    expect(promptCalls).toHaveLength(0)
  })

  test("should inject for background task session (subagent)", async () => {
    // #given - main session set, background task session registered
    setMainSession("main-session")
    const bgTaskSession = "bg-task-session"
    subagentSessions.add(bgTaskSession)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - background task session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID: bgTaskSession } },
    })

    // #then - continuation injected for background task session
    await new Promise(r => setTimeout(r, 2500))
    expect(promptCalls.length).toBe(1)
    expect(promptCalls[0].sessionID).toBe(bgTaskSession)
  })



  test("should cancel countdown on user message after grace period", async () => {
    // #given - session starting countdown
    const sessionID = "main-cancel"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #when - wait past grace period (500ms), then user sends message
    await new Promise(r => setTimeout(r, 600))
    await hook.handler({
      event: {
        type: "message.updated",
        properties: { info: { sessionID, role: "user" } }
      },
    })

    // #then - wait past countdown time and verify no injection (countdown was cancelled)
    await new Promise(r => setTimeout(r, 2500))
    expect(promptCalls).toHaveLength(0)
  })

  test("should ignore user message within grace period", async () => {
    // #given - session starting countdown
    const sessionID = "main-grace"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #when - user message arrives within grace period (immediately)
    await hook.handler({
      event: {
        type: "message.updated",
        properties: { info: { sessionID, role: "user" } }
      },
    })

    // #then - countdown should continue (message was ignored)
    // wait past 2s countdown and verify injection happens
    await new Promise(r => setTimeout(r, 2500))
    expect(promptCalls).toHaveLength(1)
  })

  test("should cancel countdown on assistant activity", async () => {
    // #given - session starting countdown
    const sessionID = "main-assistant"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #when - assistant starts responding
    await new Promise(r => setTimeout(r, 500))
    await hook.handler({
      event: {
        type: "message.part.updated",
        properties: { info: { sessionID, role: "assistant" } }
      },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected (cancelled)
    expect(promptCalls).toHaveLength(0)
  })

  test("should cancel countdown on tool execution", async () => {
    // #given - session starting countdown
    const sessionID = "main-tool"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #when - tool starts executing
    await new Promise(r => setTimeout(r, 500))
    await hook.handler({
      event: { type: "tool.execute.before", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected (cancelled)
    expect(promptCalls).toHaveLength(0)
  })

  test("should skip injection during recovery mode", async () => {
    // #given - session in recovery mode
    const sessionID = "main-recovery"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - mark as recovering
    hook.markRecovering(sessionID)

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected
    expect(promptCalls).toHaveLength(0)
  })

  test("should inject after recovery complete", async () => {
    // #given - session was in recovery, now complete
    const sessionID = "main-recovery-done"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - mark as recovering then complete
    hook.markRecovering(sessionID)
    hook.markRecoveryComplete(sessionID)

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - continuation injected
    expect(promptCalls.length).toBe(1)
  })

  test("should cleanup on session deleted", async () => {
    // #given - session starting countdown
    const sessionID = "main-delete"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #when - session is deleted during countdown
    await new Promise(r => setTimeout(r, 500))
    await hook.handler({
      event: { type: "session.deleted", properties: { info: { id: sessionID } } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation injected (cleaned up)
    expect(promptCalls).toHaveLength(0)
  })

  test("should accept skipAgents option without error", async () => {
    // #given - session with skipAgents configured for Prometheus
    const sessionID = "main-prometheus-option"
    setMainSession(sessionID)

    // #when - create hook with skipAgents option (should not throw)
    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
      skipAgents: ["Prometheus (Planner)", "custom-agent"],
    })

    // #then - handler works without error
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 100))
    expect(toastCalls.length).toBeGreaterThanOrEqual(1)
  })

  test("should show countdown toast updates", async () => {
    // #given - session with incomplete todos
    const sessionID = "main-toast"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    // #then - multiple toast updates during countdown (2s countdown = 2 toasts: "2s" and "1s")
    await new Promise(r => setTimeout(r, 2500))
    expect(toastCalls.length).toBeGreaterThanOrEqual(2)
    expect(toastCalls[0].message).toContain("2s")
  })

  test("should not have 10s throttle between injections", async () => {
    // #given - new hook instance (no prior state)
    const sessionID = "main-no-throttle"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - first idle cycle completes
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })
    await new Promise(r => setTimeout(r, 3500))

    // #then - first injection happened
    expect(promptCalls.length).toBe(1)

    // #when - immediately trigger second idle (no 10s wait needed)
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })
    await new Promise(r => setTimeout(r, 3500))

    // #then - second injection also happened (no throttle blocking)
    expect(promptCalls.length).toBe(2)
  }, { timeout: 15000 })







  test("should NOT skip for non-abort errors even if immediately before idle", async () => {
    // #given - session with incomplete todos
    const sessionID = "main-noabort-error"
    setMainSession(sessionID)

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - non-abort error occurs (e.g., network error, API error)
    await hook.handler({
      event: {
        type: "session.error",
        properties: {
          sessionID,
          error: { name: "NetworkError", message: "Connection failed" }
        }
      },
    })

    // #when - session goes idle immediately after
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 2500))

    // #then - continuation injected (non-abort errors don't block)
    expect(promptCalls.length).toBe(1)
  })





  // ============================================================
  // API-BASED ABORT DETECTION TESTS
  // These tests verify that abort is detected by checking
  // the last assistant message's error field via session.messages API
  // ============================================================

  test("should skip injection when last assistant message has MessageAbortedError", async () => {
    // #given - session where last assistant message was aborted
    const sessionID = "main-api-abort"
    setMainSession(sessionID)

    mockMessages = [
      { info: { id: "msg-1", role: "user" } },
      { info: { id: "msg-2", role: "assistant", error: { name: "MessageAbortedError", data: { message: "The operation was aborted" } } } },
    ]

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation (last message was aborted)
    expect(promptCalls).toHaveLength(0)
  })

  test("should inject when last assistant message has no error", async () => {
    // #given - session where last assistant message completed normally
    const sessionID = "main-api-no-error"
    setMainSession(sessionID)

    mockMessages = [
      { info: { id: "msg-1", role: "user" } },
      { info: { id: "msg-2", role: "assistant" } },
    ]

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - continuation injected (no abort)
    expect(promptCalls.length).toBe(1)
  })

  test("should inject when last message is from user (not assistant)", async () => {
    // #given - session where last message is from user
    const sessionID = "main-api-user-last"
    setMainSession(sessionID)

    mockMessages = [
      { info: { id: "msg-1", role: "assistant" } },
      { info: { id: "msg-2", role: "user" } },
    ]

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - continuation injected (last message is user, not aborted assistant)
    expect(promptCalls.length).toBe(1)
  })

  test("should skip when last assistant message has any abort-like error", async () => {
    // #given - session where last assistant message has AbortError (DOMException style)
    const sessionID = "main-api-abort-dom"
    setMainSession(sessionID)

    mockMessages = [
      { info: { id: "msg-1", role: "user" } },
      { info: { id: "msg-2", role: "assistant", error: { name: "AbortError" } } },
    ]

    const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})

    // #when - session goes idle
    await hook.handler({
      event: { type: "session.idle", properties: { sessionID } },
    })

    await new Promise(r => setTimeout(r, 3000))

    // #then - no continuation (abort error detected)
    expect(promptCalls).toHaveLength(0)
  })
})
