import {
	type ClineMessage,
	type ExtensionMessage,
	isIdleAsk,
	isResumableAsk,
	isInteractiveAsk,
	isNonBlockingAsk,
} from "@roo-code/types"

import { AgentLoopState, detectAgentState } from "../agent-state.js"
import { createMockClient } from "../extension-client.js"

function createMessage(overrides: Partial<ClineMessage>): ClineMessage {
	return { ts: Date.now() + Math.random() * 1000, type: "say", ...overrides }
}

function createStateMessage(messages: ClineMessage[], mode?: string): ExtensionMessage {
	return { type: "state", state: { clineMessages: messages, mode } } as ExtensionMessage
}

describe("detectAgentState", () => {
	describe("NO_TASK state", () => {
		it("should return NO_TASK for empty messages array", () => {
			const state = detectAgentState([])
			expect(state.state).toBe(AgentLoopState.NO_TASK)
			expect(state.isWaitingForInput).toBe(false)
			expect(state.isRunning).toBe(false)
		})

		it("should return NO_TASK for undefined messages", () => {
			const state = detectAgentState(undefined as unknown as ClineMessage[])
			expect(state.state).toBe(AgentLoopState.NO_TASK)
		})
	})

	describe("STREAMING state", () => {
		it("should detect streaming when partial is true", () => {
			const messages = [createMessage({ type: "ask", ask: "tool", partial: true })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
			expect(state.isWaitingForInput).toBe(false)
		})

		it("should detect streaming when api_req_started has no cost", () => {
			const messages = [
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ tokensIn: 100 }), // No cost field.
				}),
			]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
		})

		it("should NOT be streaming when api_req_started has cost", () => {
			const messages = [
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001, tokensIn: 100 }),
				}),
			]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.isStreaming).toBe(false)
		})
	})

	describe("WAITING_FOR_INPUT state", () => {
		it("should detect waiting for tool approval", () => {
			const messages = [createMessage({ type: "ask", ask: "tool", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.isWaitingForInput).toBe(true)
			expect(state.currentAsk).toBe("tool")
			expect(state.requiredAction).toBe("approve")
		})

		it("should detect waiting for command approval", () => {
			const messages = [createMessage({ type: "ask", ask: "command", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.currentAsk).toBe("command")
			expect(state.requiredAction).toBe("approve")
		})

		it("should detect waiting for followup answer", () => {
			const messages = [createMessage({ type: "ask", ask: "followup", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.currentAsk).toBe("followup")
			expect(state.requiredAction).toBe("answer")
		})

		it("should detect waiting for use_mcp_server approval", () => {
			const messages = [createMessage({ type: "ask", ask: "use_mcp_server", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.requiredAction).toBe("approve")
		})
	})

	describe("IDLE state", () => {
		it("should detect completion_result as idle", () => {
			const messages = [createMessage({ type: "ask", ask: "completion_result", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.isWaitingForInput).toBe(true)
			expect(state.requiredAction).toBe("start_task")
		})

		it("should detect api_req_failed as idle", () => {
			const messages = [createMessage({ type: "ask", ask: "api_req_failed", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.requiredAction).toBe("retry_or_new_task")
		})

		it("should detect mistake_limit_reached as idle", () => {
			const messages = [createMessage({ type: "ask", ask: "mistake_limit_reached", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.requiredAction).toBe("proceed_or_new_task")
		})

		it("should detect auto_approval_max_req_reached as idle", () => {
			const messages = [createMessage({ type: "ask", ask: "auto_approval_max_req_reached", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.requiredAction).toBe("start_new_task")
		})

		it("should detect resume_completed_task as idle", () => {
			const messages = [createMessage({ type: "ask", ask: "resume_completed_task", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.requiredAction).toBe("start_new_task")
		})
	})

	describe("RESUMABLE state", () => {
		it("should detect resume_task as resumable", () => {
			const messages = [createMessage({ type: "ask", ask: "resume_task", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RESUMABLE)
			expect(state.isWaitingForInput).toBe(true)
			expect(state.requiredAction).toBe("resume_or_abandon")
		})
	})

	describe("RUNNING state", () => {
		it("should detect running for say messages", () => {
			const messages = [
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001 }),
				}),
				createMessage({ say: "text", text: "Working on it..." }),
			]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.isRunning).toBe(true)
			expect(state.isWaitingForInput).toBe(false)
		})

		it("should detect running for command_output (non-blocking)", () => {
			const messages = [createMessage({ type: "ask", ask: "command_output", partial: false })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.requiredAction).toBe("continue_or_abort")
		})
	})
})

describe("Type Guards", () => {
	describe("isIdleAsk", () => {
		it("should return true for idle asks", () => {
			expect(isIdleAsk("completion_result")).toBe(true)
			expect(isIdleAsk("api_req_failed")).toBe(true)
			expect(isIdleAsk("mistake_limit_reached")).toBe(true)
			expect(isIdleAsk("auto_approval_max_req_reached")).toBe(true)
			expect(isIdleAsk("resume_completed_task")).toBe(true)
		})

		it("should return false for non-idle asks", () => {
			expect(isIdleAsk("tool")).toBe(false)
			expect(isIdleAsk("followup")).toBe(false)
			expect(isIdleAsk("resume_task")).toBe(false)
		})
	})

	describe("isInteractiveAsk", () => {
		it("should return true for interactive asks", () => {
			expect(isInteractiveAsk("tool")).toBe(true)
			expect(isInteractiveAsk("command")).toBe(true)
			expect(isInteractiveAsk("followup")).toBe(true)
			expect(isInteractiveAsk("use_mcp_server")).toBe(true)
		})

		it("should return false for non-interactive asks", () => {
			expect(isInteractiveAsk("completion_result")).toBe(false)
			expect(isInteractiveAsk("command_output")).toBe(false)
		})
	})

	describe("isResumableAsk", () => {
		it("should return true for resumable asks", () => {
			expect(isResumableAsk("resume_task")).toBe(true)
		})

		it("should return false for non-resumable asks", () => {
			expect(isResumableAsk("completion_result")).toBe(false)
			expect(isResumableAsk("tool")).toBe(false)
		})
	})

	describe("isNonBlockingAsk", () => {
		it("should return true for non-blocking asks", () => {
			expect(isNonBlockingAsk("command_output")).toBe(true)
		})

		it("should return false for blocking asks", () => {
			expect(isNonBlockingAsk("tool")).toBe(false)
			expect(isNonBlockingAsk("followup")).toBe(false)
		})
	})
})

describe("ExtensionClient", () => {
	describe("State queries", () => {
		it("should return NO_TASK when not initialized", () => {
			const { client } = createMockClient()
			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
			expect(client.isInitialized()).toBe(false)
		})

		it("should update state when receiving messages", () => {
			const { client } = createMockClient()

			const message = createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })])

			client.handleMessage(message)

			expect(client.isInitialized()).toBe(true)
			expect(client.getCurrentState()).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(client.isWaitingForInput()).toBe(true)
			expect(client.getCurrentAsk()).toBe("tool")
		})
	})

	describe("Event emission", () => {
		it("should emit stateChange events", () => {
			const { client } = createMockClient()
			const stateChanges: AgentLoopState[] = []

			client.onStateChange((event) => {
				stateChanges.push(event.currentState.state)
			})

			client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))

			expect(stateChanges).toContain(AgentLoopState.WAITING_FOR_INPUT)
		})

		it("should emit waitingForInput events", () => {
			const { client } = createMockClient()
			const waitingEvents: string[] = []

			client.onWaitingForInput((event) => {
				waitingEvents.push(event.ask)
			})

			client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "followup", partial: false })]))

			expect(waitingEvents).toContain("followup")
		})

		it("should allow unsubscribing from events", () => {
			const { client } = createMockClient()
			let callCount = 0

			const unsubscribe = client.onStateChange(() => {
				callCount++
			})

			client.handleMessage(createStateMessage([createMessage({ say: "text" })]))
			expect(callCount).toBe(1)

			unsubscribe()

			client.handleMessage(createStateMessage([createMessage({ say: "text", ts: Date.now() + 1 })]))
			expect(callCount).toBe(1) // Should not increase.
		})

		it("should emit modeChanged events", () => {
			const { client } = createMockClient()
			const modeChanges: { previousMode: string | undefined; currentMode: string }[] = []

			client.onModeChanged((event) => {
				modeChanges.push(event)
			})

			// Set initial mode
			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))

			expect(modeChanges).toHaveLength(1)
			expect(modeChanges[0]).toEqual({ previousMode: undefined, currentMode: "code" })

			// Change mode
			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "architect"))

			expect(modeChanges).toHaveLength(2)
			expect(modeChanges[1]).toEqual({ previousMode: "code", currentMode: "architect" })
		})

		it("should not emit modeChanged when mode stays the same", () => {
			const { client } = createMockClient()
			let modeChangeCount = 0

			client.onModeChanged(() => {
				modeChangeCount++
			})

			// Set initial mode
			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))
			expect(modeChangeCount).toBe(1)

			// Same mode - should not emit
			client.handleMessage(createStateMessage([createMessage({ say: "text", ts: Date.now() + 1 })], "code"))
			expect(modeChangeCount).toBe(1)
		})
	})

	describe("Response methods", () => {
		it("should send approve response", () => {
			const { client, sentMessages } = createMockClient()

			client.approve()

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "askResponse",
				askResponse: "yesButtonClicked",
				text: undefined,
				images: undefined,
			})
		})

		it("should send reject response", () => {
			const { client, sentMessages } = createMockClient()

			client.reject()

			expect(sentMessages).toHaveLength(1)
			const msg = sentMessages[0]
			expect(msg).toBeDefined()
			expect(msg?.askResponse).toBe("noButtonClicked")
		})

		it("should send text response", () => {
			const { client, sentMessages } = createMockClient()

			client.respond("My answer", ["image-data"])

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "askResponse",
				askResponse: "messageResponse",
				text: "My answer",
				images: ["image-data"],
			})
		})

		it("should send newTask message", () => {
			const { client, sentMessages } = createMockClient()

			client.newTask("Build a web app")

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "newTask",
				text: "Build a web app",
				images: undefined,
			})
		})

		it("should send clearTask message", () => {
			const { client, sentMessages } = createMockClient()

			client.clearTask()

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "clearTask",
			})
		})

		it("should send cancelTask message", () => {
			const { client, sentMessages } = createMockClient()

			client.cancelTask()

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "cancelTask",
			})
		})

		it("should send terminal continue operation", () => {
			const { client, sentMessages } = createMockClient()

			client.continueTerminal()

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "terminalOperation",
				terminalOperation: "continue",
			})
		})

		it("should send terminal abort operation", () => {
			const { client, sentMessages } = createMockClient()

			client.abortTerminal()

			expect(sentMessages).toHaveLength(1)
			expect(sentMessages[0]).toEqual({
				type: "terminalOperation",
				terminalOperation: "abort",
			})
		})
	})

	describe("Message handling", () => {
		it("should handle JSON string messages", () => {
			const { client } = createMockClient()

			const message = JSON.stringify(
				createStateMessage([createMessage({ type: "ask", ask: "completion_result", partial: false })]),
			)

			client.handleMessage(message)

			expect(client.getCurrentState()).toBe(AgentLoopState.IDLE)
		})

		it("should ignore invalid JSON", () => {
			const { client } = createMockClient()

			client.handleMessage("not valid json")

			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
		})

		it("should handle messageUpdated messages", () => {
			const { client } = createMockClient()

			// First, set initial state.
			client.handleMessage(
				createStateMessage([createMessage({ ts: 123, type: "ask", ask: "tool", partial: true })]),
			)

			expect(client.isStreaming()).toBe(true)

			// Now update the message.
			client.handleMessage({
				type: "messageUpdated",
				clineMessage: createMessage({ ts: 123, type: "ask", ask: "tool", partial: false }),
			})

			expect(client.isStreaming()).toBe(false)
			expect(client.isWaitingForInput()).toBe(true)
		})
	})

	describe("Reset functionality", () => {
		it("should reset state", () => {
			const { client } = createMockClient()

			client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))

			expect(client.isInitialized()).toBe(true)
			expect(client.getCurrentState()).toBe(AgentLoopState.WAITING_FOR_INPUT)

			client.reset()

			expect(client.isInitialized()).toBe(false)
			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
		})

		it("should reset mode on reset", () => {
			const { client } = createMockClient()

			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))
			expect(client.getCurrentMode()).toBe("code")

			client.reset()

			expect(client.getCurrentMode()).toBeUndefined()
		})
	})

	describe("Mode tracking", () => {
		it("should return undefined mode when not initialized", () => {
			const { client } = createMockClient()
			expect(client.getCurrentMode()).toBeUndefined()
		})

		it("should track mode from state messages", () => {
			const { client } = createMockClient()

			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))

			expect(client.getCurrentMode()).toBe("code")
		})

		it("should update mode when it changes", () => {
			const { client } = createMockClient()

			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))
			expect(client.getCurrentMode()).toBe("code")

			client.handleMessage(createStateMessage([createMessage({ say: "text", ts: Date.now() + 1 })], "architect"))
			expect(client.getCurrentMode()).toBe("architect")
		})

		it("should preserve mode when state message has no mode", () => {
			const { client } = createMockClient()

			// Set initial mode
			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "code"))
			expect(client.getCurrentMode()).toBe("code")

			// State update without mode - should preserve existing mode
			client.handleMessage(createStateMessage([createMessage({ say: "text", ts: Date.now() + 1 })]))
			expect(client.getCurrentMode()).toBe("code")
		})

		it("should preserve mode when task is cleared", () => {
			const { client } = createMockClient()

			client.handleMessage(createStateMessage([createMessage({ say: "text" })], "architect"))
			expect(client.getCurrentMode()).toBe("architect")

			client.clearTask()
			// Mode should be preserved after clear
			expect(client.getCurrentMode()).toBe("architect")
		})
	})
})

describe("Integration", () => {
	it("should handle a complete task flow", () => {
		const { client } = createMockClient()
		const states: AgentLoopState[] = []

		client.onStateChange((event) => {
			states.push(event.currentState.state)
		})

		// 1. Task starts, API request begins.
		client.handleMessage(
			createStateMessage([
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({}), // No cost = streaming.
				}),
			]),
		)
		expect(client.isStreaming()).toBe(true)

		// 2. API request completes.
		client.handleMessage(
			createStateMessage([
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001 }),
				}),
				createMessage({ say: "text", text: "I'll help you with that." }),
			]),
		)
		expect(client.isStreaming()).toBe(false)
		expect(client.isRunning()).toBe(true)

		// 3. Tool ask (partial).
		client.handleMessage(
			createStateMessage([
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001 }),
				}),
				createMessage({ say: "text", text: "I'll help you with that." }),
				createMessage({ type: "ask", ask: "tool", partial: true }),
			]),
		)
		expect(client.isStreaming()).toBe(true)

		// 4. Tool ask (complete).
		client.handleMessage(
			createStateMessage([
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001 }),
				}),
				createMessage({ say: "text", text: "I'll help you with that." }),
				createMessage({ type: "ask", ask: "tool", partial: false }),
			]),
		)
		expect(client.isWaitingForInput()).toBe(true)
		expect(client.getCurrentAsk()).toBe("tool")

		// 5. User approves, task completes.
		client.handleMessage(
			createStateMessage([
				createMessage({
					say: "api_req_started",
					text: JSON.stringify({ cost: 0.001 }),
				}),
				createMessage({ say: "text", text: "I'll help you with that." }),
				createMessage({ type: "ask", ask: "tool", partial: false }),
				createMessage({ say: "text", text: "File created." }),
				createMessage({ type: "ask", ask: "completion_result", partial: false }),
			]),
		)
		expect(client.getCurrentState()).toBe(AgentLoopState.IDLE)
		expect(client.getCurrentAsk()).toBe("completion_result")

		// Verify we saw the expected state transitions.
		expect(states).toContain(AgentLoopState.STREAMING)
		expect(states).toContain(AgentLoopState.RUNNING)
		expect(states).toContain(AgentLoopState.WAITING_FOR_INPUT)
		expect(states).toContain(AgentLoopState.IDLE)
	})
})

describe("Edge Cases", () => {
	describe("Messages with missing or empty text field", () => {
		it("should handle ask message with missing text field", () => {
			const messages = [createMessage({ type: "ask", ask: "tool", partial: false })]
			// Text is undefined by default.
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.currentAsk).toBe("tool")
		})

		it("should handle ask message with empty text field", () => {
			const messages = [createMessage({ type: "ask", ask: "followup", partial: false, text: "" })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.currentAsk).toBe("followup")
		})

		it("should handle say message with missing text field", () => {
			const messages = [createMessage({ say: "text" })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
		})
	})

	describe("api_req_started edge cases", () => {
		it("should handle api_req_started with empty text field as streaming", () => {
			const messages = [createMessage({ say: "api_req_started", text: "" })]
			const state = detectAgentState(messages)
			// Empty text is treated as "no text yet" = still in progress (streaming).
			// This matches the behavior: !message.text is true for "" (falsy).
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
		})

		it("should handle api_req_started with invalid JSON", () => {
			const messages = [createMessage({ say: "api_req_started", text: "not valid json" })]
			const state = detectAgentState(messages)
			// Invalid JSON should not crash, should return not streaming.
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.isStreaming).toBe(false)
		})

		it("should handle api_req_started with null text", () => {
			const messages = [createMessage({ say: "api_req_started", text: undefined })]
			const state = detectAgentState(messages)
			// No text means still in progress (streaming).
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
		})

		it("should handle api_req_started with cost of 0", () => {
			const messages = [createMessage({ say: "api_req_started", text: JSON.stringify({ cost: 0 }) })]
			const state = detectAgentState(messages)
			// cost: 0 is defined (not undefined), so NOT streaming.
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.isStreaming).toBe(false)
		})

		it("should handle api_req_started with cost of null", () => {
			const messages = [createMessage({ say: "api_req_started", text: JSON.stringify({ cost: null }) })]
			const state = detectAgentState(messages)
			// cost: null is defined (not undefined), so NOT streaming.
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.isStreaming).toBe(false)
		})

		it("should find api_req_started when it's not the last message", () => {
			const messages = [
				createMessage({ say: "api_req_started", text: JSON.stringify({ tokensIn: 100 }) }), // No cost = streaming
				createMessage({ say: "text", text: "Some text" }),
			]
			const state = detectAgentState(messages)
			// Last message is say:text, but api_req_started has no cost.
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
		})
	})

	describe("Rapid state transitions", () => {
		it("should handle multiple rapid state changes", () => {
			const { client } = createMockClient()
			const states: AgentLoopState[] = []

			client.onStateChange((event) => {
				states.push(event.currentState.state)
			})

			// Rapid updates.
			client.handleMessage(createStateMessage([createMessage({ say: "text" })]))
			client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: true })]))
			client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))
			client.handleMessage(
				createStateMessage([createMessage({ type: "ask", ask: "completion_result", partial: false })]),
			)

			// Should have tracked all transitions.
			expect(states.length).toBeGreaterThanOrEqual(3)
			expect(states).toContain(AgentLoopState.STREAMING)
			expect(states).toContain(AgentLoopState.WAITING_FOR_INPUT)
			expect(states).toContain(AgentLoopState.IDLE)
		})
	})

	describe("Message array edge cases", () => {
		it("should handle single message array", () => {
			const messages = [createMessage({ say: "text", text: "Hello" })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
			expect(state.lastMessage).toBeDefined()
			expect(state.lastMessageTs).toBe(messages[0]!.ts)
		})

		it("should use last message for state detection", () => {
			// Multiple messages, last one determines state.
			const messages = [
				createMessage({ type: "ask", ask: "tool", partial: false }),
				createMessage({ say: "text", text: "Tool executed" }),
				createMessage({ type: "ask", ask: "completion_result", partial: false }),
			]
			const state = detectAgentState(messages)
			// Last message is completion_result, so IDLE.
			expect(state.state).toBe(AgentLoopState.IDLE)
			expect(state.currentAsk).toBe("completion_result")
		})

		it("should handle very long message arrays", () => {
			// Create many messages.
			const messages: ClineMessage[] = []

			for (let i = 0; i < 100; i++) {
				messages.push(createMessage({ say: "text", text: `Message ${i}` }))
			}

			messages.push(createMessage({ type: "ask", ask: "followup", partial: false }))

			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state.currentAsk).toBe("followup")
		})
	})

	describe("State message edge cases", () => {
		it("should handle state message with empty clineMessages", () => {
			const { client } = createMockClient()
			client.handleMessage({ type: "state", state: { clineMessages: [] } } as unknown as ExtensionMessage)
			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
			expect(client.isInitialized()).toBe(true)
		})

		it("should handle state message with missing clineMessages", () => {
			const { client } = createMockClient()

			client.handleMessage({
				type: "state",
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				state: {} as any,
			})

			// Should not crash, state should remain unchanged.
			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
		})

		it("should handle state message with missing state field", () => {
			const { client } = createMockClient()

			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			client.handleMessage({ type: "state" } as any)

			// Should not crash
			expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
		})
	})

	describe("Partial to complete transitions", () => {
		it("should transition from streaming to waiting when partial becomes false", () => {
			const ts = Date.now()
			const messages1 = [createMessage({ ts, type: "ask", ask: "tool", partial: true })]
			const messages2 = [createMessage({ ts, type: "ask", ask: "tool", partial: false })]

			const state1 = detectAgentState(messages1)
			const state2 = detectAgentState(messages2)

			expect(state1.state).toBe(AgentLoopState.STREAMING)
			expect(state1.isWaitingForInput).toBe(false)

			expect(state2.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
			expect(state2.isWaitingForInput).toBe(true)
		})

		it("should handle partial say messages", () => {
			const messages = [createMessage({ say: "text", text: "Typing...", partial: true })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.STREAMING)
			expect(state.isStreaming).toBe(true)
		})
	})

	describe("Unknown message types", () => {
		it("should handle unknown ask types gracefully", () => {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const messages = [createMessage({ type: "ask", ask: "unknown_type" as any, partial: false })]
			const state = detectAgentState(messages)
			// Unknown ask type should default to RUNNING.
			expect(state.state).toBe(AgentLoopState.RUNNING)
		})

		it("should handle unknown say types gracefully", () => {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const messages = [createMessage({ say: "unknown_say_type" as any })]
			const state = detectAgentState(messages)
			expect(state.state).toBe(AgentLoopState.RUNNING)
		})
	})
})
