import type { CallbackManager } from '@langchain/core/callbacks/manager';
import type { IExecuteFunctions, ISupplyDataFunctions } from 'n8n-workflow';
import { mock } from 'vitest-mock-extended';

import { buildTracingMetadata, getTracingConfig } from './tracing';

describe('getTracingConfig', () => {
	const mockWorkflow = {
		id: 'workflow-123',
		name: 'Test Workflow',
		active: true,
	};

	const mockNode = {
		name: 'AI Agent',
		type: 'n8n-nodes-langchain.agent',
		typeVersion: 3,
	};

	describe('with IExecuteFunctions context', () => {
		it('should return correct runName format', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext);

			expect(result.runName).toBe('[Test Workflow] AI Agent');
		});

		it('should return correct metadata', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext);

			expect(result.metadata).toEqual({
				execution_id: 'exec-456',
				workflow: mockWorkflow,
				node: 'AI Agent',
			});
		});

		it('should include parent callback manager when available', () => {
			const mockCallbackManager = mock<CallbackManager>();
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');
			mockContext.getParentCallbackManager.mockReturnValue(mockCallbackManager);

			const result = getTracingConfig(mockContext);

			expect(result.callbacks).toBe(mockCallbackManager);
		});

		it('should set callbacks to undefined when getParentCallbackManager returns undefined', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');
			mockContext.getParentCallbackManager.mockReturnValue(undefined);

			const result = getTracingConfig(mockContext);

			expect(result.callbacks).toBeUndefined();
		});

		it('should merge additionalMetadata when provided', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext, {
				additionalMetadata: {
					custom_field: 'custom_value',
					another_field: 123,
				},
			});

			expect(result.metadata).toEqual({
				execution_id: 'exec-456',
				workflow: mockWorkflow,
				node: 'AI Agent',
				custom_field: 'custom_value',
				another_field: 123,
			});
		});

		it('should handle empty additionalMetadata', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext, { additionalMetadata: {} });

			expect(result.metadata).toEqual({
				execution_id: 'exec-456',
				workflow: mockWorkflow,
				node: 'AI Agent',
			});
		});
	});

	describe('with ISupplyDataFunctions context', () => {
		it('should return correct config without getParentCallbackManager', () => {
			// ISupplyDataFunctions doesn't have getParentCallbackManager
			const mockContext = {
				getWorkflow: vi.fn().mockReturnValue(mockWorkflow),
				getNode: vi.fn().mockReturnValue(mockNode),
				getExecutionId: vi.fn().mockReturnValue('exec-789'),
			} as unknown as ISupplyDataFunctions;

			const result = getTracingConfig(mockContext);

			expect(result.runName).toBe('[Test Workflow] AI Agent');
			expect(result.metadata).toEqual({
				execution_id: 'exec-789',
				workflow: mockWorkflow,
				node: 'AI Agent',
			});
			expect(result.callbacks).toBeUndefined();
		});
	});

	describe('edge cases', () => {
		it('should handle workflow names with special characters', () => {
			const specialWorkflow = {
				...mockWorkflow,
				name: 'Workflow [with] special (chars)',
			};
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(specialWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext);

			expect(result.runName).toBe('[Workflow [with] special (chars)] AI Agent');
		});

		it('should handle node names with special characters', () => {
			const specialNode = {
				...mockNode,
				name: 'Node "with" quotes',
			};
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(specialNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext);

			expect(result.runName).toBe('[Test Workflow] Node "with" quotes');
		});

		it('should use default empty config when none provided', () => {
			const mockContext = mock<IExecuteFunctions>();
			mockContext.getWorkflow.mockReturnValue(mockWorkflow);
			mockContext.getNode.mockReturnValue(mockNode as ReturnType<IExecuteFunctions['getNode']>);
			mockContext.getExecutionId.mockReturnValue('exec-456');

			const result = getTracingConfig(mockContext);

			// Should not throw and should have correct structure
			expect(result).toHaveProperty('runName');
			expect(result).toHaveProperty('metadata');
			expect(result).toHaveProperty('callbacks');
		});
	});
});

describe('buildTracingMetadata', () => {
	describe('legacy untyped format', () => {
		it('should map valid entries into metadata', () => {
			const result = buildTracingMetadata([
				{ key: 'team', value: 'ai' } as any,
				{ key: 'trace_id', value: 'abc123' } as any,
			]);

			expect(result).toEqual({ team: 'ai', trace_id: 'abc123' });
		});

		it('should ignore empty keys and values', () => {
			const result = buildTracingMetadata([
				{ key: '', value: 'ignored' } as any,
				{ key: '   ', value: 'ignored' } as any,
				{ key: 'valid', value: '' } as any,
				{ key: 'ok', value: 'yes' } as any,
			]);

			expect(result).toEqual({ ok: 'yes' });
		});
	});

	describe('typed format', () => {
		describe('string type', () => {
			it('should handle string values', () => {
				const result = buildTracingMetadata([
					{ key: 'session_id', type: 'stringValue', stringValue: 'abc123' },
				]);

				expect(result).toEqual({ session_id: 'abc123' });
			});

			it('should skip empty string values', () => {
				const result = buildTracingMetadata([
					{ key: 'empty', type: 'stringValue', stringValue: '' },
					{ key: 'valid', type: 'stringValue', stringValue: 'value' },
				]);

				expect(result).toEqual({ valid: 'value' });
			});
		});

		describe('number type', () => {
			it('should convert number strings to numbers', () => {
				const result = buildTracingMetadata([
					{ key: 'integer', type: 'numberValue', numberValue: '100' },
					{ key: 'float', type: 'numberValue', numberValue: '3.14' },
					{ key: 'negative', type: 'numberValue', numberValue: '-42' },
				]);

				expect(result).toEqual({ integer: 100, float: 3.14, negative: -42 });
			});

			it('should skip invalid number values', () => {
				const result = buildTracingMetadata([
					{ key: 'invalid', type: 'numberValue', numberValue: 'not-a-number' },
					{ key: 'valid', type: 'numberValue', numberValue: '123' },
				]);

				expect(result).toEqual({ valid: 123 });
			});
		});

		describe('boolean type', () => {
			it('should convert boolean strings to booleans', () => {
				const result = buildTracingMetadata([
					{ key: 'is_active', type: 'booleanValue', booleanValue: 'true' },
					{ key: 'is_disabled', type: 'booleanValue', booleanValue: 'false' },
				]);

				expect(result).toEqual({ is_active: true, is_disabled: false });
			});
		});

		describe('array type', () => {
			it('should parse array strings into arrays', () => {
				const result = buildTracingMetadata([
					{ key: 'tags', type: 'arrayValue', arrayValue: '["ai", "agent", "test"]' },
					{ key: 'numbers', type: 'arrayValue', arrayValue: '[1, 2, 3]' },
				]);

				expect(result).toEqual({
					tags: ['ai', 'agent', 'test'],
					numbers: [1, 2, 3],
				});
			});

			it('should skip invalid array values', () => {
				const result = buildTracingMetadata([
					{ key: 'invalid', type: 'arrayValue', arrayValue: 'not-an-array' },
					{ key: 'valid', type: 'arrayValue', arrayValue: '["test"]' },
				]);

				expect(result).toEqual({ valid: ['test'] });
			});
		});

		describe('object type', () => {
			it('should parse JSON strings into objects', () => {
				const result = buildTracingMetadata([
					{
						key: 'config',
						type: 'objectValue',
						objectValue: '{"threshold": 0.8, "enabled": true}',
					},
				]);

				expect(result).toEqual({ config: { threshold: 0.8, enabled: true } });
			});

			it('should skip invalid JSON values', () => {
				const result = buildTracingMetadata([
					{ key: 'invalid', type: 'objectValue', objectValue: '{invalid json}' },
					{ key: 'valid', type: 'objectValue', objectValue: '{"key": "value"}' },
				]);

				expect(result).toEqual({ valid: { key: 'value' } });
			});
		});

		it('should handle mixed types in one call', () => {
			const result = buildTracingMetadata([
				{ key: 'session_id', type: 'stringValue', stringValue: 'abc123' },
				{ key: 'user_id', type: 'numberValue', numberValue: '42' },
				{ key: 'is_active', type: 'booleanValue', booleanValue: 'true' },
				{ key: 'tags', type: 'arrayValue', arrayValue: '["ai", "test"]' },
				{ key: 'config', type: 'objectValue', objectValue: '{"threshold": 0.8}' },
			]);

			expect(result).toEqual({
				session_id: 'abc123',
				user_id: 42,
				is_active: true,
				tags: ['ai', 'test'],
				config: { threshold: 0.8 },
			});
		});

		it('should trim whitespace from keys', () => {
			const result = buildTracingMetadata([
				{ key: '  session_id  ', type: 'stringValue', stringValue: 'test' },
			]);

			expect(result).toEqual({ session_id: 'test' });
		});

		it('should skip entries with empty keys', () => {
			const result = buildTracingMetadata([
				{ key: '', type: 'stringValue', stringValue: 'ignored' },
				{ key: '   ', type: 'stringValue', stringValue: 'ignored' },
				{ key: 'valid', type: 'stringValue', stringValue: 'kept' },
			]);

			expect(result).toEqual({ valid: 'kept' });
		});

		it('should skip entries with undefined or null values', () => {
			const result = buildTracingMetadata([
				{ key: 'undefined_val', type: 'stringValue', stringValue: undefined },
				{ key: 'null_val', type: 'stringValue', stringValue: null as any },
				{ key: 'valid', type: 'stringValue', stringValue: 'kept' },
			]);

			expect(result).toEqual({ valid: 'kept' });
		});
	});

	it('should handle undefined or empty input', () => {
		expect(buildTracingMetadata(undefined)).toEqual({});
		expect(buildTracingMetadata([])).toEqual({});
	});
});
