import type { Mocked } from 'vitest';
import { mock } from 'vitest-mock-extended';
import type { WorkflowEntity } from '@n8n/db';
import type { Request, Response } from 'express';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { DynamicCredentialCorsService } from '../services/dynamic-credential-cors.service';
import type { DynamicCredentialWebService } from '../services/dynamic-credential-web.service';
import { WorkflowStatusController } from '../workflow-status.controller';
import type { CredentialResolverWorkflowService } from '../services/credential-resolver-workflow.service';
import type { UrlService } from '@/services/url.service';
import type { WorkflowFinderService } from '@/workflows/workflow-finder.service';
import type { GlobalConfig } from '@n8n/config';

vi.mock('../utils', () => ({
	getDynamicCredentialMiddlewares: vi.fn(() => undefined),
}));

describe('WorkflowStatusController', () => {
	let controller: WorkflowStatusController;
	let mockService: Mocked<CredentialResolverWorkflowService>;
	let mockUrlService: Mocked<UrlService>;
	let mockGlobalConfig: Mocked<GlobalConfig>;
	let mockDynamicCredentialCorsService: Mocked<DynamicCredentialCorsService>;
	let mockDynamicCredentialWebService: Mocked<DynamicCredentialWebService>;
	let mockWorkflowFinderService: Mocked<WorkflowFinderService>;

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

		mockService = {
			getWorkflowStatus: vi.fn(),
		} as unknown as Mocked<CredentialResolverWorkflowService>;

		mockUrlService = {
			getInstanceBaseUrl: vi.fn().mockReturnValue('https://n8n.example.com'),
		} as unknown as Mocked<UrlService>;

		mockGlobalConfig = {
			endpoints: {
				rest: 'rest',
			},
		} as unknown as Mocked<GlobalConfig>;

		mockDynamicCredentialCorsService = {
			applyCorsHeadersIfEnabled: vi.fn(),
			preflightHandler: vi.fn(),
		} as unknown as Mocked<DynamicCredentialCorsService>;

		mockDynamicCredentialWebService = mock<DynamicCredentialWebService>();
		mockDynamicCredentialWebService.getCredentialContextFromRequest = vi.fn().mockReturnValue({
			identity: 'mock-token',
			version: 1,
			metadata: {},
		});

		mockWorkflowFinderService = mock<WorkflowFinderService>();
		// Default: caller can access the workflow
		mockWorkflowFinderService.findWorkflowForUser.mockResolvedValue(mock<WorkflowEntity>());

		controller = new WorkflowStatusController(
			mockService,
			mockUrlService,
			mockGlobalConfig,
			mockDynamicCredentialCorsService,
			mockDynamicCredentialWebService,
			mockWorkflowFinderService,
		);
	});

	describe('in-app access control', () => {
		it('returns 404 when an authenticated user cannot access the workflow', async () => {
			mockWorkflowFinderService.findWorkflowForUser.mockResolvedValue(null);
			const req = mock<Request>();
			req.params = { workflowId: 'foreign-workflow' };
			req.headers = { authorization: 'Bearer token-123' };
			// in-app session caller carries a user (set as own prop, as auth middleware does)
			(req as unknown as { user?: unknown }).user = { id: 'user-123' };
			const res = mock<Response>();

			await expect(controller.checkWorkflowForExecution(req, res)).rejects.toThrow(NotFoundError);
			expect(mockService.getWorkflowStatus).not.toHaveBeenCalled();
		});

		it('skips the user access check for external (token-only) callers', async () => {
			mockService.getWorkflowStatus.mockResolvedValue([]);
			const req = mock<Request>();
			req.params = { workflowId: 'wf-1' };
			req.headers = { authorization: 'Bearer token-123' };
			// external resolver caller: no session user
			(req as unknown as { user?: unknown }).user = undefined;
			const res = mock<Response>();

			await controller.checkWorkflowForExecution(req, res);
			expect(mockWorkflowFinderService.findWorkflowForUser).not.toHaveBeenCalled();
			expect(mockService.getWorkflowStatus).toHaveBeenCalled();
		});
	});

	describe('checkWorkflowForExecution', () => {
		it('should throw BadRequestError when workflow ID is missing', async () => {
			const req = mock<Request>();
			req.params = { workflowId: '' };
			req.headers = { authorization: 'Bearer token-123' };
			const res = mock<Response>();

			await expect(controller.checkWorkflowForExecution(req, res)).rejects.toThrow(BadRequestError);
			await expect(controller.checkWorkflowForExecution(req, res)).rejects.toThrow(
				'Workflow ID is missing',
			);
			expect(mockService.getWorkflowStatus).not.toHaveBeenCalled();
		});

		it('should return readyToExecute true when all credentials are configured', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'oauth2Api',
				},
				{
					credentialId: 'cred-2',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'httpBasicAuth',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.readyToExecute).toBe(true);
			expect(result.workflowId).toBe('workflow-1');
			expect(result.credentials).toHaveLength(2);
		});

		it('should return readyToExecute false when any credential is missing', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'oauth2Api',
				},
				{
					credentialId: 'cred-2',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'missing',
					credentialType: 'httpBasicAuth',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.readyToExecute).toBe(false);
			expect(result.workflowId).toBe('workflow-1');
			expect(result.credentials).toHaveLength(2);
		});

		it('should include authorizationUrl with encoded resolverId in response', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'oauth2Api',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.credentials?.[0].authorizationUrl).toBe(
				'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver-1',
			);
			expect(result.credentials?.[0].revokeUrl).toBe(
				'https://n8n.example.com/rest/credentials/cred-1/revoke?resolverId=resolver-1',
			);
		});

		it('should properly encode special characters in resolverId', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver/with/special chars',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'oauth2Api',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.credentials?.[0].authorizationUrl).toBe(
				'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver%2Fwith%2Fspecial%20chars',
			);
			expect(result.credentials?.[0].revokeUrl).toBe(
				'https://n8n.example.com/rest/credentials/cred-1/revoke?resolverId=resolver%2Fwith%2Fspecial%20chars',
			);
		});

		it('should return absolute URLs with base path from UrlService', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'configured',
					credentialType: 'oauth2Api',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			// Verify that the URL is absolute and starts with the base URL
			expect(result.credentials?.[0].authorizationUrl).toMatch(/^https:\/\//);
			expect(result.credentials?.[0].authorizationUrl).toContain('https://n8n.example.com');
			expect(result.credentials?.[0].revokeUrl).toMatch(/^https:\/\//);
			expect(result.credentials?.[0].revokeUrl).toContain('https://n8n.example.com');
			expect(mockUrlService.getInstanceBaseUrl).toHaveBeenCalled();
		});

		it('should map service response to DTO correctly', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					resolverId: 'resolver-1',
					credentialName: 'My OAuth2 Credential',
					status: 'missing',
					credentialType: 'oauth2Api',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result).toEqual({
				workflowId: 'workflow-1',
				readyToExecute: false,
				credentials: [
					{
						credentialId: 'cred-1',
						credentialStatus: 'missing',
						credentialName: 'My OAuth2 Credential',
						credentialType: 'oauth2Api',
						authorizationUrl:
							'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver-1',
						revokeUrl:
							'https://n8n.example.com/rest/credentials/cred-1/revoke?resolverId=resolver-1',
					},
				],
			});
		});

		it('should return readyToExecute false when credential has resolver_missing status', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([
				{
					credentialId: 'cred-1',
					credentialName: 'My OAuth2 Credential',
					status: 'resolver_missing',
					credentialType: 'oauth2Api',
				},
			]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.readyToExecute).toBe(false);
			expect(result.workflowId).toBe('workflow-1');
			expect(result.credentials).toHaveLength(1);
			expect(result.credentials?.[0].credentialStatus).toBe('resolver_missing');
			expect(result.credentials?.[0].authorizationUrl).toBeUndefined();
			expect(result.credentials?.[0].revokeUrl).toBeUndefined();
		});

		it('should return readyToExecute true for empty credentials array', async () => {
			const req = mock<Request>({
				params: { workflowId: 'workflow-1' },
				headers: { authorization: 'Bearer token-123' },
			});
			const res = mock<Response>();

			mockService.getWorkflowStatus.mockResolvedValue([]);

			const result = await controller.checkWorkflowForExecution(req, res);

			expect(result.readyToExecute).toBe(true);
			expect(result.credentials).toEqual([]);
		});
	});
});
