/* eslint-disable @typescript-eslint/no-explicit-any */

// npx vitest run src/__tests__/TelemetryClient.test.ts

import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"

import { CloudTelemetryClient as TelemetryClient } from "../TelemetryClient.js"

const mockFetch = vi.fn()
global.fetch = mockFetch as any

describe("TelemetryClient", () => {
	const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
		return instance[propertyName]
	}

	let mockAuthService: any
	let mockSettingsService: any

	beforeEach(() => {
		vi.clearAllMocks()

		// Create a mock AuthService instead of using the singleton
		mockAuthService = {
			getSessionToken: vi.fn().mockReturnValue("mock-token"),
			getState: vi.fn().mockReturnValue("active-session"),
			isAuthenticated: vi.fn().mockReturnValue(true),
			hasActiveSession: vi.fn().mockReturnValue(true),
		}

		// Create a mock SettingsService
		mockSettingsService = {
			getSettings: vi.fn().mockReturnValue({
				cloudSettings: {
					recordTaskMessages: true,
				},
			}),
			getUserSettings: vi.fn().mockReturnValue({
				features: {},
				settings: {
					taskSyncEnabled: true,
				},
				version: 1,
			}),
			isTaskSyncEnabled: vi.fn().mockReturnValue(true),
		}

		mockFetch.mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue({}),
		})

		vi.spyOn(console, "info").mockImplementation(() => {})
		vi.spyOn(console, "error").mockImplementation(() => {})
	})

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

	describe("isEventCapturable", () => {
		it("should return true for events not in exclude list", () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
				client,
				"isEventCapturable",
			).bind(client)

			expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
			expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
			expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
			expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
		})

		it("should return false for events in exclude list", () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
				client,
				"isEventCapturable",
			).bind(client)

			expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
		})

		it("should return true for TASK_MESSAGE events when isTaskSyncEnabled returns true", () => {
			mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)

			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
				client,
				"isEventCapturable",
			).bind(client)

			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
		})

		it("should return false for TASK_MESSAGE events when isTaskSyncEnabled returns false", () => {
			mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)

			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
				client,
				"isEventCapturable",
			).bind(client)

			expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
		})
	})

	describe("getEventProperties", () => {
		it("should merge provider properties with event properties", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const mockProvider: TelemetryPropertiesProvider = {
				getTelemetryProperties: vi.fn().mockResolvedValue({
					appVersion: "1.0.0",
					vscodeVersion: "1.60.0",
					platform: "darwin",
					editorName: "vscode",
					language: "en",
					mode: "code",
				}),
			}

			client.setProvider(mockProvider)

			const getEventProperties = getPrivateProperty<
				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
			>(client, "getEventProperties").bind(client)

			const result = await getEventProperties({
				event: TelemetryEventName.TASK_CREATED,
				properties: {
					customProp: "value",
					mode: "override", // This should override the provider's mode.
				},
			})

			expect(result).toEqual({
				appVersion: "1.0.0",
				vscodeVersion: "1.60.0",
				platform: "darwin",
				editorName: "vscode",
				language: "en",
				mode: "override", // Event property takes precedence.
				customProp: "value",
			})

			expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
		})

		it("should handle errors from provider gracefully", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const mockProvider: TelemetryPropertiesProvider = {
				getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
			}

			const consoleErrorSpy = vi.spyOn(console, "error")

			client.setProvider(mockProvider)

			const getEventProperties = getPrivateProperty<
				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
			>(client, "getEventProperties").bind(client)

			const result = await getEventProperties({
				event: TelemetryEventName.TASK_CREATED,
				properties: { customProp: "value" },
			})

			expect(result).toEqual({ customProp: "value" })
			expect(consoleErrorSpy).toHaveBeenCalledWith(
				expect.stringContaining("Error getting telemetry properties: Provider error"),
			)
		})

		it("should return event properties when no provider is set", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const getEventProperties = getPrivateProperty<
				(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
			>(client, "getEventProperties").bind(client)

			const result = await getEventProperties({
				event: TelemetryEventName.TASK_CREATED,
				properties: { customProp: "value" },
			})

			expect(result).toEqual({ customProp: "value" })
		})
	})

	describe("capture", () => {
		it("should not capture events that are not capturable", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.capture({
				event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
				properties: { test: "value" },
			})

			expect(mockFetch).not.toHaveBeenCalled()
		})

		it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => {
			mockSettingsService.getSettings.mockReturnValue({
				cloudSettings: {
					recordTaskMessages: false,
				},
			})

			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.capture({
				event: TelemetryEventName.TASK_MESSAGE,
				properties: {
					taskId: "test-task-id",
					message: {
						ts: 1,
						type: "say",
						say: "text",
						text: "test message",
					},
				},
			})

			expect(mockFetch).not.toHaveBeenCalled()
		})

		it("should not capture TASK_MESSAGE events when isTaskSyncEnabled returns false", async () => {
			mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)

			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.capture({
				event: TelemetryEventName.TASK_MESSAGE,
				properties: {
					taskId: "test-task-id",
					message: {
						ts: 1,
						type: "say",
						say: "text",
						text: "test message",
					},
				},
			})

			expect(mockFetch).not.toHaveBeenCalled()
			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
		})

		it("should not send request when schema validation fails", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.capture({
				event: TelemetryEventName.TASK_CREATED,
				properties: { test: "value" },
			})

			expect(mockFetch).not.toHaveBeenCalled()
			expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
		})

		it("should send request when event is capturable and validation passes", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const providerProperties = {
				appName: "roo-code",
				appVersion: "1.0.0",
				vscodeVersion: "1.60.0",
				platform: "darwin",
				editorName: "vscode",
				language: "en",
				mode: "code",
			}

			const eventProperties = {
				taskId: "test-task-id",
			}

			const mockValidatedData = {
				type: TelemetryEventName.TASK_CREATED,
				properties: {
					...providerProperties,
					taskId: "test-task-id",
				},
			}

			const mockProvider: TelemetryPropertiesProvider = {
				getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
			}

			client.setProvider(mockProvider)

			await client.capture({
				event: TelemetryEventName.TASK_CREATED,
				properties: eventProperties,
			})

			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events",
				expect.objectContaining({
					method: "POST",
					body: JSON.stringify(mockValidatedData),
				}),
			)
		})

		it("should attempt to capture TASK_MESSAGE events when isTaskSyncEnabled returns true", async () => {
			mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)

			const eventProperties = {
				appName: "roo-code",
				appVersion: "1.0.0",
				vscodeVersion: "1.60.0",
				platform: "darwin",
				editorName: "vscode",
				language: "en",
				mode: "code",
				taskId: "test-task-id",
				message: {
					ts: 1,
					type: "say",
					say: "text",
					text: "test message",
				},
			}

			const mockValidatedData = {
				type: TelemetryEventName.TASK_MESSAGE,
				properties: eventProperties,
			}

			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.capture({
				event: TelemetryEventName.TASK_MESSAGE,
				properties: eventProperties,
			})

			expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events",
				expect.objectContaining({
					method: "POST",
					body: JSON.stringify(mockValidatedData),
				}),
			)
		})

		it("should handle fetch errors gracefully", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			mockFetch.mockRejectedValue(new Error("Network error"))

			await expect(
				client.capture({
					event: TelemetryEventName.TASK_CREATED,
					properties: { test: "value" },
				}),
			).resolves.not.toThrow()
		})
	})

	describe("telemetry state methods", () => {
		it("should always return true for isTelemetryEnabled", () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)
			expect(client.isTelemetryEnabled()).toBe(true)
		})

		it("should have empty implementations for updateTelemetryState and shutdown", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)
			client.updateTelemetryState(true)
			await client.shutdown()
		})
	})

	describe("backfillMessages", () => {
		it("should not send request when not authenticated", async () => {
			mockAuthService.isAuthenticated.mockReturnValue(false)
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(mockFetch).not.toHaveBeenCalled()
		})

		it("should not send request when no session token available", async () => {
			mockAuthService.getSessionToken.mockReturnValue(null)
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(mockFetch).not.toHaveBeenCalled()
			expect(console.error).toHaveBeenCalledWith(
				"[TelemetryClient#backfillMessages] Unauthorized: No session token available.",
			)
		})

		it("should send FormData request with correct structure when authenticated", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const providerProperties = {
				appName: "roo-code",
				appVersion: "1.0.0",
				vscodeVersion: "1.60.0",
				platform: "darwin",
				editorName: "vscode",
				language: "en",
				mode: "code",
			}

			const mockProvider: TelemetryPropertiesProvider = {
				getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
			}

			client.setProvider(mockProvider)

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message 1",
				},
				{
					ts: 2,
					type: "ask" as const,
					ask: "followup" as const,
					text: "test question",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events/backfill",
				expect.objectContaining({
					method: "POST",
					headers: {
						Authorization: "Bearer mock-token",
					},
					body: expect.any(FormData),
				}),
			)

			// Verify FormData contents
			const call = mockFetch.mock.calls[0]
			const formData = call?.[1]?.body as FormData

			expect(formData.get("taskId")).toBe("test-task-id")

			// Parse and compare properties as objects since JSON.stringify order can vary
			const propertiesJson = formData.get("properties") as string
			const parsedProperties = JSON.parse(propertiesJson)
			expect(parsedProperties).toEqual({
				taskId: "test-task-id",
				...providerProperties,
			})
			// The messages are stored as a File object under the "file" key
			const fileField = formData.get("file") as File
			expect(fileField).toBeInstanceOf(File)
			expect(fileField.name).toBe("task.json")
			expect(fileField.type).toBe("application/json")

			// Read the file content to verify the messages
			const fileContent = await fileField.text()
			expect(fileContent).toBe(JSON.stringify(messages))
		})

		it("should handle provider errors gracefully", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const mockProvider: TelemetryPropertiesProvider = {
				getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
			}

			client.setProvider(mockProvider)

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events/backfill",
				expect.objectContaining({
					method: "POST",
					headers: {
						Authorization: "Bearer mock-token",
					},
					body: expect.any(FormData),
				}),
			)

			// Verify FormData contents - should still work with just taskId
			const call = mockFetch.mock.calls[0]
			const formData = call?.[1]?.body as FormData

			expect(formData.get("taskId")).toBe("test-task-id")
			expect(formData.get("properties")).toBe(
				JSON.stringify({
					taskId: "test-task-id",
				}),
			)

			// The messages are stored as a File object under the "file" key
			const fileField = formData.get("file") as File
			expect(fileField).toBeInstanceOf(File)
			expect(fileField.name).toBe("task.json")
			expect(fileField.type).toBe("application/json")

			// Read the file content to verify the messages
			const fileContent = await fileField.text()
			expect(fileContent).toBe(JSON.stringify(messages))
		})

		it("should work without provider set", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events/backfill",
				expect.objectContaining({
					method: "POST",
					headers: {
						Authorization: "Bearer mock-token",
					},
					body: expect.any(FormData),
				}),
			)

			// Verify FormData contents - should work with just taskId
			const call = mockFetch.mock.calls[0]
			const formData = call?.[1]?.body as FormData

			expect(formData.get("taskId")).toBe("test-task-id")
			expect(formData.get("properties")).toBe(
				JSON.stringify({
					taskId: "test-task-id",
				}),
			)

			// The messages are stored as a File object under the "file" key
			const fileField = formData.get("file") as File
			expect(fileField).toBeInstanceOf(File)
			expect(fileField.name).toBe("task.json")
			expect(fileField.type).toBe("application/json")

			// Read the file content to verify the messages
			const fileContent = await fileField.text()
			expect(fileContent).toBe(JSON.stringify(messages))
		})

		it("should handle fetch errors gracefully", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			mockFetch.mockRejectedValue(new Error("Network error"))

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow()

			expect(console.error).toHaveBeenCalledWith(
				expect.stringContaining(
					"[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error",
				),
			)
		})

		it("should handle HTTP error responses", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			mockFetch.mockResolvedValue({
				ok: false,
				status: 404,
				statusText: "Not Found",
			})

			const messages = [
				{
					ts: 1,
					type: "say" as const,
					say: "text" as const,
					text: "test message",
				},
			]

			await client.backfillMessages(messages, "test-task-id")

			expect(console.error).toHaveBeenCalledWith(
				"[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found",
			)
		})

		it("should handle empty messages array", async () => {
			const client = new TelemetryClient(mockAuthService, mockSettingsService)

			await client.backfillMessages([], "test-task-id")

			expect(mockFetch).toHaveBeenCalledWith(
				"https://app.roocode.com/api/events/backfill",
				expect.objectContaining({
					method: "POST",
					headers: {
						Authorization: "Bearer mock-token",
					},
					body: expect.any(FormData),
				}),
			)

			// Verify FormData contents
			const call = mockFetch.mock.calls[0]
			const formData = call?.[1]?.body as FormData

			// The messages are stored as a File object under the "file" key
			const fileField = formData.get("file") as File
			expect(fileField).toBeInstanceOf(File)
			expect(fileField.name).toBe("task.json")
			expect(fileField.type).toBe("application/json")

			// Read the file content to verify the empty messages array
			const fileContent = await fileField.text()
			expect(fileContent).toBe("[]")
		})
	})
})
