import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import {
	useChatPanelStore,
	DEFAULT_CHAT_WIDTH,
	MAX_CHAT_WIDTH,
	MIN_CHAT_WIDTH,
} from './chatPanel.store';
import { ASSISTANT_ENABLED_VIEWS } from './constants';

const ENABLED_VIEWS = ASSISTANT_ENABLED_VIEWS;
import { useAssistantStore } from '@/features/ai/assistant/assistant.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { ChatRequest } from '@/features/ai/assistant/assistant.types';
import { usePostHog } from '@/app/stores/posthog.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { defaultSettings } from '@/__tests__/defaults';
import merge from 'lodash/merge';
import { DEFAULT_POSTHOG_SETTINGS } from '@/app/stores/posthog.store.test';
import { VIEWS } from '@/app/constants';
import { reactive } from 'vue';
import * as chatAPI from '@/features/ai/assistant/assistant.api';
import * as telemetryModule from '@/app/composables/useTelemetry';
import type { Telemetry } from '@/app/plugins/telemetry';
import type { ChatUI } from '@n8n/design-system/types/assistant';

let settingsStore: ReturnType<typeof useSettingsStore>;
let posthogStore: ReturnType<typeof usePostHog>;

const apiSpy = vi.spyOn(chatAPI, 'chatWithAssistant');

const track = vi.fn();
const spy = vi.spyOn(telemetryModule, 'useTelemetry');
spy.mockImplementation(
	() =>
		({
			track,
		}) as unknown as Telemetry,
);

const setAssistantEnabled = (enabled: boolean) => {
	settingsStore.setSettings(
		merge({}, defaultSettings, {
			aiAssistant: { enabled, setup: true },
		}),
	);
};

let currentRouteName = ENABLED_VIEWS[0];
let currentRouteParams = {};
vi.mock('vue-router', () => ({
	useRoute: vi.fn(() =>
		reactive({
			path: '/',
			params: currentRouteParams,
			name: currentRouteName,
		}),
	),
	useRouter: vi.fn(),
	RouterLink: vi.fn(),
}));

describe('AI Assistant store', () => {
	beforeEach(() => {
		vi.clearAllMocks();
		currentRouteParams = {};
		setActivePinia(createPinia());
		settingsStore = useSettingsStore();
		settingsStore.setSettings(
			merge({}, defaultSettings, {
				posthog: DEFAULT_POSTHOG_SETTINGS,
			}),
		);
		window.posthog = {
			init: () => {},
			identify: () => {},
		};
		posthogStore = usePostHog();
		posthogStore.init();
		track.mockReset();
	});

	it('initializes with default values', () => {
		const assistantStore = useAssistantStore();
		const chatPanelStore = useChatPanelStore();

		expect(chatPanelStore.width).toBe(DEFAULT_CHAT_WIDTH);
		expect(assistantStore.chatMessages).toEqual([]);
		expect(chatPanelStore.isOpen).toBe(false);
		expect(assistantStore.streaming).toBeUndefined();
	});

	it('can change chat width', () => {
		const chatPanelStore = useChatPanelStore();

		chatPanelStore.updateWidth(400);
		expect(chatPanelStore.width).toBe(400);
	});

	it('should not allow chat width to be less than the minimal width', () => {
		const chatPanelStore = useChatPanelStore();

		chatPanelStore.updateWidth(100);
		expect(chatPanelStore.width).toBe(MIN_CHAT_WIDTH);
	});

	it('should not allow chat width to be more than the maximal width', () => {
		const chatPanelStore = useChatPanelStore();

		chatPanelStore.updateWidth(2000);
		expect(chatPanelStore.width).toBe(MAX_CHAT_WIDTH);
	});

	it('should open chat window', async () => {
		const chatPanelStore = useChatPanelStore();

		await chatPanelStore.open({ mode: 'assistant' });
		expect(chatPanelStore.isOpen).toBe(true);
	});

	it('should close chat window', () => {
		const chatPanelStore = useChatPanelStore();

		chatPanelStore.close();
		expect(chatPanelStore.isOpen).toBe(false);
	});

	it('can add a simple assistant message', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'message',
			role: 'assistant',
			text: 'Hello!',
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);
		expect(assistantStore.chatMessages[0]).toEqual({
			id: '1',
			type: 'text',
			role: 'assistant',
			content: 'Hello!',
			quickReplies: undefined,
			read: false,
		});
	});

	it('can add an assistant message with quick replies', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'message',
			role: 'assistant',
			text: 'Hello!',
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);
		expect(assistantStore.chatMessages[0]).toEqual({
			id: '1',
			type: 'text',
			role: 'assistant',
			content: 'Hello!',
			read: false,
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		});
	});

	it('can add an assistant code-diff message', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'code-diff',
			role: 'assistant',
			description: 'Here is the suggested code change',
			codeDiff: 'diff --git a/file1 b/file2',
			suggestionId: '1',
			solution_count: 1,
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);
		expect(assistantStore.chatMessages[0]).toEqual({
			id: '1',
			type: 'code-diff',
			role: 'assistant',
			description: 'Here is the suggested code change',
			codeDiff: 'diff --git a/file1 b/file2',
			suggestionId: '1',
			read: false,
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		});
	});

	it('can add an assistant summary message', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'summary',
			role: 'assistant',
			title: 'Summary',
			content: 'Here is the summary',
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);
		expect(assistantStore.chatMessages[0]).toEqual({
			id: '1',
			type: 'block',
			role: 'assistant',
			title: 'Summary',
			content: 'Here is the summary',
			read: false,
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		});
	});

	it('can add an agent suggestion message', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'agent-suggestion',
			role: 'assistant',
			title: 'A Suggestion',
			text: 'Here is a suggestion',
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);
		expect(assistantStore.chatMessages[0]).toEqual({
			id: '1',
			type: 'block',
			role: 'assistant',
			title: 'A Suggestion',
			content: 'Here is a suggestion',
			read: false,
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		});
	});

	it('should reset assistant chat session', () => {
		const assistantStore = useAssistantStore();

		const message: ChatRequest.MessageResponse = {
			type: 'message',
			role: 'assistant',
			text: 'Hello!',
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};
		assistantStore.addAssistantMessages([message], '1');
		expect(assistantStore.chatMessages.length).toBe(1);

		assistantStore.resetAssistantChat();
		expect(assistantStore.chatMessages).toEqual([]);
		expect(assistantStore.currentSessionId).toBeUndefined();
	});

	it('should not show assistant if disabled in settings', () => {
		const assistantStore = useAssistantStore();

		setAssistantEnabled(false);
		expect(assistantStore.isAssistantEnabled).toBe(false);
	});

	it('should show assistant if all conditions are met', () => {
		const assistantStore = useAssistantStore();

		setAssistantEnabled(true);
		expect(assistantStore.isAssistantEnabled).toBe(true);
	});

	it('should not show assistant if on a settings page', () => {
		currentRouteName = VIEWS.SSO_SETTINGS;
		const assistantStore = useAssistantStore();

		setAssistantEnabled(true);
		expect(assistantStore.isAssistantEnabled).toBe(true);
	});

	[VIEWS.PROJECTS_CREDENTIALS, VIEWS.TEMPLATE_SETUP, VIEWS.CREDENTIALS].forEach((view) => {
		it(`should show assistant if on ${view} page`, () => {
			currentRouteName = view;
			const assistantStore = useAssistantStore();

			setAssistantEnabled(true);
			expect(assistantStore.isAssistantEnabled).toBe(true);
		});
	});

	[
		{ view: VIEWS.WORKFLOW, nodeId: 'nodeId' },
		{ view: VIEWS.NEW_WORKFLOW },
		{ view: VIEWS.EXECUTION_DEBUG },
	].forEach(({ view, nodeId }) => {
		it(`should show ai assistant floating button if on ${view} page`, () => {
			currentRouteName = view;
			currentRouteParams = nodeId ? { nodeId } : {};

			const workflowsStore = useWorkflowsStore();
			workflowsStore.activeNode = () => ({
				id: 'test-node',
				name: 'Test Node',
				type: 'test',
				typeVersion: 1,
				position: [0, 0],
				parameters: {},
			});

			const assistantStore = useAssistantStore();

			setAssistantEnabled(true);
			expect(assistantStore.hideAssistantFloatingButton).toBe(false);
		});
	});

	[{ view: VIEWS.WORKFLOW }, { view: VIEWS.NEW_WORKFLOW }].forEach(({ view }) => {
		it(`should hide ai assistant floating button if on canvas of ${view} page`, () => {
			currentRouteName = view;

			const workflowsStore = useWorkflowsStore();
			workflowsStore.activeNode = () => null;

			const assistantStore = useAssistantStore();

			setAssistantEnabled(true);
			expect(assistantStore.hideAssistantFloatingButton).toBe(true);
		});
	});

	it('should initialize assistant chat session on node error', async () => {
		const context: ChatRequest.ErrorContext = {
			error: {
				description: '',
				message: 'Hey',
				name: 'NodeOperationError',
			},
			node: {
				id: '1',
				type: 'n8n-nodes-base.stopAndError',
				typeVersion: 1,
				name: 'Stop and Error',
				position: [250, 250],
				parameters: {},
			},
		};
		const assistantStore = useAssistantStore();
		await assistantStore.initErrorHelper(context);
		expect(apiSpy).toHaveBeenCalled();
	});

	it('should call telemetry for opening assistant with error', async () => {
		const context: ChatRequest.ErrorContext = {
			error: {
				description: '',
				message: 'Hey',
				name: 'NodeOperationError',
			},
			node: {
				id: '1',
				type: 'n8n-nodes-base.stopAndError',
				typeVersion: 1,
				name: 'Stop and Error',
				position: [250, 250],
				parameters: {},
			},
		};
		const mockSessionId = 'test';

		const assistantStore = useAssistantStore();
		apiSpy.mockImplementation((_ctx, _payload, onMessage) => {
			onMessage({
				messages: [],
				sessionId: mockSessionId,
			});
		});

		await assistantStore.initErrorHelper(context);
		expect(apiSpy).toHaveBeenCalled();
		expect(assistantStore.currentSessionId).toEqual(mockSessionId);

		assistantStore.trackUserOpenedAssistant({
			task: 'error',
			source: 'error',
			has_existing_session: true,
		});
		expect(track).toHaveBeenCalledWith('Assistant session started', {
			chat_session_id: 'test',
			node_type: 'n8n-nodes-base.stopAndError',
			task: 'error',
			credential_type: undefined,
		});

		expect(track).toHaveBeenCalledWith('User opened assistant', {
			chat_session_id: 'test',
			error: {
				description: '',
				message: 'Hey',
				name: 'NodeOperationError',
			},
			has_existing_session: true,
			node_type: 'n8n-nodes-base.stopAndError',
			source: 'error',
			task: 'error',
			workflow_id: '',
			instance_id: '',
			canvas_status: 'empty',
		});
	});

	it('should call telemetry for opening assistant with build_with_ai source on empty canvas', () => {
		const assistantStore = useAssistantStore();
		const workflowsStore = useWorkflowsStore();

		// Ensure canvas is empty
		workflowsStore.workflow.nodes = [];

		assistantStore.trackUserOpenedAssistant({
			task: 'placeholder',
			source: 'build_with_ai',
			has_existing_session: false,
		});

		expect(track).toHaveBeenCalledWith('User opened assistant', {
			source: 'build_with_ai',
			task: 'placeholder',
			has_existing_session: false,
			instance_id: '',
			workflow_id: '',
			canvas_status: 'empty',
			node_type: undefined,
			error: undefined,
			chat_session_id: undefined,
		});
	});

	it('should call telemetry for opening assistant on existing workflow', () => {
		const assistantStore = useAssistantStore();
		const workflowsStore = useWorkflowsStore();

		// Add a node to the workflow
		workflowsStore.workflow.nodes = [
			{
				id: '1',
				type: 'n8n-nodes-base.start',
				typeVersion: 1,
				name: 'Start',
				position: [250, 250],
				parameters: {},
			},
		];

		assistantStore.trackUserOpenedAssistant({
			task: 'placeholder',
			source: 'canvas',
			has_existing_session: false,
		});

		expect(track).toHaveBeenCalledWith('User opened assistant', {
			source: 'canvas',
			task: 'placeholder',
			has_existing_session: false,
			instance_id: '',
			workflow_id: '',
			canvas_status: 'existing_workflow',
			node_type: undefined,
			error: undefined,
			chat_session_id: undefined,
		});
	});

	it('should call the function again if retry is called after handleServiceError', async () => {
		const mockFn = vi.fn();

		const assistantStore = useAssistantStore();

		assistantStore.handleServiceError(new Error('test error'), '125', mockFn);
		expect(assistantStore.chatMessages.length).toBe(1);
		const message = assistantStore.chatMessages[0];
		expect(message.type).toBe('error');

		const errorMessage = message as ChatUI.ErrorMessage;
		expect(errorMessage.retry).toBeDefined();

		// This simulates the button click from the UI
		await errorMessage.retry?.();

		expect(mockFn).toHaveBeenCalled();
	});

	it('should not add error message when handleServiceError receives an AbortError', () => {
		const assistantStore = useAssistantStore();
		const abortError = new Error('The operation was aborted');
		abortError.name = 'AbortError';

		assistantStore.handleServiceError(abortError, '125');
		expect(assistantStore.chatMessages.length).toBe(0);
		expect(assistantStore.streaming).toBe(false);
	});

	it('should pass abort signal to chatWithAssistant when initializing support chat', async () => {
		const assistantStore = useAssistantStore();
		setAssistantEnabled(true);

		await assistantStore.initSupportChat('hello');

		expect(apiSpy).toHaveBeenCalledWith(
			expect.anything(),
			expect.anything(),
			expect.any(Function),
			expect.any(Function),
			expect.any(Function),
			expect.any(AbortSignal),
		);
	});

	it('should pass abort signal to chatWithAssistant when initializing error helper', async () => {
		const context: ChatRequest.ErrorContext = {
			error: {
				description: '',
				message: 'Hey',
				name: 'NodeOperationError',
			},
			node: {
				id: '1',
				type: 'n8n-nodes-base.stopAndError',
				typeVersion: 1,
				name: 'Stop and Error',
				position: [250, 250],
				parameters: {},
			},
		};

		const assistantStore = useAssistantStore();
		await assistantStore.initErrorHelper(context);

		expect(apiSpy).toHaveBeenCalledWith(
			expect.anything(),
			expect.anything(),
			expect.any(Function),
			expect.any(Function),
			expect.any(Function),
			expect.any(AbortSignal),
		);
	});

	it('should abort streaming and stop when abortStreaming is called', async () => {
		const assistantStore = useAssistantStore();
		const mockSessionId = 'test-session';

		apiSpy.mockImplementationOnce((_ctx, _payload, onMessage) => {
			onMessage({
				messages: [{ type: 'message', role: 'assistant', text: 'Hello!' }],
				sessionId: mockSessionId,
			});
			// Don't call onDone to simulate ongoing streaming
		});

		await assistantStore.initSupportChat('hello');
		expect(assistantStore.streaming).toBe(true);

		assistantStore.abortStreaming();
		expect(assistantStore.streaming).toBe(false);
	});

	it('should properly clear messages on retry in a chat session', async () => {
		const assistantStore = useAssistantStore();
		const mockSessionId = 'mockSessionId';

		const message: ChatRequest.MessageResponse = {
			type: 'message',
			role: 'assistant',
			text: 'Hello!',
			quickReplies: [
				{ text: 'Yes', type: 'text' },
				{ text: 'No', type: 'text' },
			],
		};

		apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
			onMessage({ messages: [message], sessionId: mockSessionId });
			onDone();
		});
		apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) =>
			onError(new Error('test error')),
		);
		apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
			onMessage({ messages: [message], sessionId: mockSessionId });
			onDone();
		});

		await assistantStore.initSupportChat('hello');

		expect(assistantStore.chatMessages.length).toBe(2);
		expect(assistantStore.chatMessages[0].type).toBe('text');
		expect(assistantStore.chatMessages[1].type).toBe('text');

		await assistantStore.sendMessage({ text: 'test' });

		expect(assistantStore.chatMessages.length).toBe(4);
		expect(assistantStore.chatMessages[0].type).toBe('text');
		expect(assistantStore.chatMessages[1].type).toBe('text');
		expect(assistantStore.chatMessages[2].type).toBe('text');
		expect(assistantStore.chatMessages[3].type).toBe('error');

		expect(assistantStore.chatMessages[3]).toHaveProperty('retry');
		// This simulates the functionality triggered from the consumer (e.g. UI Button)
		await (assistantStore.chatMessages[3] as ChatUI.ErrorMessage).retry?.();

		expect(assistantStore.chatMessages.length).toBe(4);
		expect(assistantStore.chatMessages[0].type).toBe('text');
		expect(assistantStore.chatMessages[1].type).toBe('text');
		expect(assistantStore.chatMessages[2].type).toBe('text');
		expect(assistantStore.chatMessages[3].type).toBe('text');
	});
});
