// npx vitest run src/components/chat/__tests__/ChatView.notification-sound.spec.tsx

import React from "react"
import { render, waitFor } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"

import ChatView, { ChatViewProps } from "../ChatView"

// Define minimal types needed for testing
interface ClineMessage {
	type: "say" | "ask"
	say?: string
	ask?: string
	ts: number
	text?: string
	partial?: boolean
}

interface QueuedMessage {
	id: string
	text: string
	images?: string[]
}

interface ExtensionState {
	version: string
	clineMessages: ClineMessage[]
	taskHistory: any[]
	shouldShowAnnouncement: boolean
	messageQueue?: QueuedMessage[]
	[key: string]: any
}

// Mock vscode API
vi.mock("@src/utils/vscode", () => ({
	vscode: {
		postMessage: vi.fn(),
	},
}))

// Mock use-sound hook
const mockPlayFunction = vi.fn()
vi.mock("use-sound", () => ({
	default: vi.fn().mockImplementation(() => {
		return [mockPlayFunction]
	}),
}))

// Mock components that use ESM dependencies
vi.mock("../ChatRow", () => ({
	default: function MockChatRow({ message }: { message: ClineMessage }) {
		return <div data-testid="chat-row">{JSON.stringify(message)}</div>
	},
}))

vi.mock("../AutoApproveMenu", () => ({
	default: () => null,
}))

// Mock VersionIndicator
vi.mock("../../common/VersionIndicator", () => ({
	default: vi.fn(() => null),
}))

vi.mock("../Announcement", () => ({
	default: function MockAnnouncement({ hideAnnouncement }: { hideAnnouncement: () => void }) {
		// eslint-disable-next-line @typescript-eslint/no-require-imports
		const React = require("react")
		return React.createElement(
			"div",
			{ "data-testid": "announcement-modal" },
			React.createElement("div", null, "What's New"),
			React.createElement("button", { onClick: hideAnnouncement }, "Close"),
		)
	},
}))

// Mock DismissibleUpsell component
vi.mock("@/components/common/DismissibleUpsell", () => ({
	default: function MockDismissibleUpsell({ children }: { children: React.ReactNode }) {
		return <div data-testid="dismissible-upsell">{children}</div>
	},
}))

// Mock QueuedMessages component
vi.mock("../QueuedMessages", () => ({
	QueuedMessages: function MockQueuedMessages({
		queue = [],
		onRemove,
	}: {
		queue?: Array<{ id: string; text: string; images?: string[] }>
		onRemove?: (index: number) => void
		onUpdate?: (index: number, newText: string) => void
	}) {
		if (!queue || queue.length === 0) {
			return null
		}
		return (
			<div data-testid="queued-messages">
				{queue.map((msg, index) => (
					<div key={msg.id}>
						<span>{msg.text}</span>
						<button aria-label="Remove message" onClick={() => onRemove?.(index)}>
							Remove
						</button>
					</div>
				))}
			</div>
		)
	},
}))

// Mock RooTips component
vi.mock("@src/components/welcome/RooTips", () => ({
	default: function MockRooTips() {
		return <div data-testid="roo-tips">Tips content</div>
	},
}))

// Mock RooHero component
vi.mock("@src/components/welcome/RooHero", () => ({
	default: function MockRooHero() {
		return <div data-testid="roo-hero">Hero content</div>
	},
}))

// Mock TelemetryBanner component
vi.mock("../common/TelemetryBanner", () => ({
	default: function MockTelemetryBanner() {
		return null // Don't render anything to avoid interference
	},
}))

// Mock i18n
vi.mock("react-i18next", () => ({
	useTranslation: () => ({
		t: (key: string, options?: any) => {
			if (key === "chat:versionIndicator.ariaLabel" && options?.version) {
				return `Version ${options.version}`
			}
			return key
		},
	}),
	initReactI18next: {
		type: "3rdParty",
		init: () => {},
	},
	Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => {
		return <>{children || i18nKey}</>
	},
}))

interface ChatTextAreaProps {
	onSend: (value: string) => void
	inputValue?: string
	sendingDisabled?: boolean
	placeholderText?: string
	selectedImages?: string[]
	shouldDisableImages?: boolean
}

const mockInputRef = React.createRef<HTMLInputElement>()
const mockFocus = vi.fn()

vi.mock("../ChatTextArea", () => {
	// eslint-disable-next-line @typescript-eslint/no-require-imports
	const mockReact = require("react")

	const ChatTextAreaComponent = mockReact.forwardRef(function MockChatTextArea(
		props: ChatTextAreaProps,
		ref: React.ForwardedRef<{ focus: () => void }>,
	) {
		// Use useImperativeHandle to expose the mock focus method
		mockReact.useImperativeHandle(ref, () => ({
			focus: mockFocus,
		}))

		return (
			<div data-testid="chat-textarea">
				<input
					ref={mockInputRef}
					type="text"
					onChange={(e) => {
						// With message queueing, onSend is always called (it handles queueing internally)
						props.onSend(e.target.value)
					}}
					data-sending-disabled={props.sendingDisabled}
				/>
			</div>
		)
	})

	return {
		default: ChatTextAreaComponent,
		ChatTextArea: ChatTextAreaComponent, // Export as named export too
	}
})

// Mock VSCode components
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
	VSCodeButton: function MockVSCodeButton({
		children,
		onClick,
		appearance,
	}: {
		children: React.ReactNode
		onClick?: () => void
		appearance?: string
	}) {
		return (
			<button onClick={onClick} data-appearance={appearance}>
				{children}
			</button>
		)
	},
	VSCodeTextField: function MockVSCodeTextField({
		value,
		onInput,
		placeholder,
	}: {
		value?: string
		onInput?: (e: { target: { value: string } }) => void
		placeholder?: string
	}) {
		return (
			<input
				type="text"
				value={value}
				onChange={(e) => onInput?.({ target: { value: e.target.value } })}
				placeholder={placeholder}
			/>
		)
	},
	VSCodeLink: function MockVSCodeLink({ children, href }: { children: React.ReactNode; href?: string }) {
		return <a href={href}>{children}</a>
	},
}))

// Mock window.postMessage to trigger state hydration
const mockPostMessage = (state: Partial<ExtensionState>) => {
	window.postMessage(
		{
			type: "state",
			state: {
				version: "1.0.0",
				clineMessages: [],
				taskHistory: [],
				shouldShowAnnouncement: false,
				cloudIsAuthenticated: false,
				telemetrySetting: "enabled",
				messageQueue: [],
				...state,
			},
		},
		"*",
	)
}

const defaultProps: ChatViewProps = {
	isHidden: false,
	showAnnouncement: false,
	hideAnnouncement: () => {},
}

const queryClient = new QueryClient()

const renderChatView = (props: Partial<ChatViewProps> = {}) => {
	return render(
		<ExtensionStateContextProvider>
			<QueryClientProvider client={queryClient}>
				<ChatView {...defaultProps} {...props} />
			</QueryClientProvider>
		</ExtensionStateContextProvider>,
	)
}

describe("ChatView - Notification Sound with Queued Messages", () => {
	beforeEach(() => vi.clearAllMocks())

	it("does not play celebration sound when completion_result is received with queued messages", async () => {
		renderChatView()

		// First hydrate state with initial task
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [
				{
					id: "msg-1",
					text: "This is a queued message",
					images: [],
				},
			],
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
			],
		})

		// Clear any initial calls
		mockPlayFunction.mockClear()

		// Add completion result with queued messages present
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [
				{
					id: "msg-1",
					text: "This is a queued message",
					images: [],
				},
			],
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
				{
					type: "ask",
					ask: "completion_result",
					ts: Date.now(),
					text: "Task completed successfully",
					partial: false, // Ensure it's not partial
				},
			],
		})

		// Wait a bit to ensure the effect would have run
		await waitFor(
			() => {
				// Should NOT play sound when there are queued messages
				expect(mockPlayFunction).not.toHaveBeenCalled()
			},
			{ timeout: 1000 },
		)
	})

	it("plays celebration sound when completion_result is received with empty message queue", async () => {
		renderChatView()

		// First hydrate state with initial task
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [], // Empty queue
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
			],
		})

		// Clear any initial calls
		mockPlayFunction.mockClear()

		// Add completion result with empty message queue
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [], // Empty queue
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
				{
					type: "ask",
					ask: "completion_result",
					ts: Date.now(),
					text: "Task completed successfully",
					partial: false, // Ensure it's not partial
				},
			],
		})

		// Wait for sound to be played
		await waitFor(() => {
			// Should play sound when message queue is empty
			expect(mockPlayFunction).toHaveBeenCalled()
		})
	})

	it("does not play celebration sound when completion_result is received with multiple queued messages", async () => {
		renderChatView()

		// First hydrate state with initial task
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [
				{
					id: "msg-1",
					text: "First queued message",
					images: [],
				},
				{
					id: "msg-2",
					text: "Second queued message",
					images: [],
				},
			],
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
			],
		})

		// Clear any initial calls
		mockPlayFunction.mockClear()

		// Add completion result with multiple queued messages
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [
				{
					id: "msg-1",
					text: "First queued message",
					images: [],
				},
				{
					id: "msg-2",
					text: "Second queued message",
					images: [],
				},
			],
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
				{
					type: "ask",
					ask: "completion_result",
					ts: Date.now(),
					text: "Task completed successfully",
					partial: false, // Ensure it's not partial
				},
			],
		})

		// Wait a bit to ensure the effect would have run
		await waitFor(
			() => {
				// Should NOT play sound when there are multiple queued messages
				expect(mockPlayFunction).not.toHaveBeenCalled()
			},
			{ timeout: 1000 },
		)
	})

	it("does not play celebration sound when completion_result is partial", async () => {
		renderChatView()

		// First hydrate state with initial task
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [], // Empty queue
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
			],
		})

		// Clear any initial calls
		mockPlayFunction.mockClear()

		// Add partial completion result
		mockPostMessage({
			soundEnabled: true, // Enable sound
			messageQueue: [], // Empty queue
			clineMessages: [
				{
					type: "say",
					say: "task",
					ts: Date.now() - 2000,
					text: "Initial task",
				},
				{
					type: "ask",
					ask: "completion_result",
					ts: Date.now(),
					text: "Task completed successfully",
					partial: true, // Partial message
				},
			],
		})

		// Wait a bit to ensure the effect would have run
		await waitFor(
			() => {
				// Should NOT play sound when message is partial
				expect(mockPlayFunction).not.toHaveBeenCalled()
			},
			{ timeout: 1000 },
		)
	})
})

describe("ChatView - Sound Debounce", () => {
	beforeEach(() => vi.clearAllMocks())

	it("should not play the same sound type twice within 100ms", async () => {
		const now = 1_000_000
		const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now)

		renderChatView()

		// Hydrate with initial task
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [{ type: "say", say: "task", ts: now - 2000, text: "Initial task" }],
		})

		// Clear any setup calls
		mockPlayFunction.mockClear()

		// First completion_result — should trigger celebration sound
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [
				{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
				{ type: "ask", ask: "completion_result", ts: now, text: "Task completed", partial: false },
			],
		})

		await waitFor(() => {
			expect(mockPlayFunction).toHaveBeenCalledTimes(1)
		})

		// Simulate only 50ms passing — still inside the 100ms debounce window
		dateNowSpy.mockReturnValue(now + 50)

		// Second completion_result with slightly different content to force useDeepCompareEffect re-fire
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [
				{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
				{ type: "ask", ask: "completion_result", ts: now + 50, text: "Task completed again", partial: false },
			],
		})

		// Allow time for the second state update to propagate through React effects
		await new Promise((resolve) => setTimeout(resolve, 300))

		// Debounce should have prevented the second play
		expect(mockPlayFunction).toHaveBeenCalledTimes(1)

		dateNowSpy.mockRestore()
	})

	it("should allow playing the same sound type again after 100ms", async () => {
		const now = 1_000_000
		const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now)

		renderChatView()

		// Hydrate with initial task
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [{ type: "say", say: "task", ts: now - 2000, text: "Initial task" }],
		})

		// Clear any setup calls
		mockPlayFunction.mockClear()

		// First completion_result — triggers sound
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [
				{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
				{ type: "ask", ask: "completion_result", ts: now, text: "Task completed", partial: false },
			],
		})

		await waitFor(() => {
			expect(mockPlayFunction).toHaveBeenCalledTimes(1)
		})

		// Advance past the 100ms debounce window
		dateNowSpy.mockReturnValue(now + 101)

		// Second completion_result with different content to trigger a fresh effect cycle
		mockPostMessage({
			soundEnabled: true,
			messageQueue: [],
			clineMessages: [
				{ type: "say", say: "task", ts: now - 2000, text: "Initial task" },
				{ type: "ask", ask: "completion_result", ts: now + 101, text: "Second task completed", partial: false },
			],
		})

		// This time the debounce window has elapsed — sound should play again
		await waitFor(() => {
			expect(mockPlayFunction).toHaveBeenCalledTimes(2)
		})

		dateNowSpy.mockRestore()
	})
})
