import {
	createTeamProject,
	getPersonalProject,
	linkUserToProject,
	createWorkflow,
	randomCredentialPayload,
	testDb,
	mockInstance,
} from '@n8n/backend-test-utils';
import type { Project, User, Role } from '@n8n/db';

import { UserManagementMailer } from '@/user-management/email';

import { createCustomRoleWithScopeSlugs, cleanupRolesAndScopes } from '../shared/db/roles';
import { createOwner, createMember } from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';

const testServer = utils.setupTestServer({
	endpointGroups: ['workflows', 'credentials'],
	enabledFeatures: ['feat:sharing', 'feat:customRoles'],
	quotas: {
		'quota:maxTeamProjects': -1,
	},
});

// Foundation users and projects
let owner: User;
let testUser: User;
let teamProject: Project;
let testUserPersonalProject: Project;

// Custom role definitions (8 total - covering all combinations)
let workflowReadOnlyRole: Role;
let workflowAllOperationsRole: Role;
let credentialReadOnlyRole: Role;
let credentialAllOperationsRole: Role;

// Authentication agents
let ownerAgent: SuperAgentTest;
let testUserAgent: SuperAgentTest;

// Test data - created fresh for each test
let testWorkflowId: string;
let testWorkflowVersionId: string;
let testCredentialId: string;

describe('Resource Access Control Matrix Tests', () => {
	beforeAll(async () => {
		mockInstance(UserManagementMailer, {
			invite: jest.fn(),
			passwordReset: jest.fn(),
		});

		// Create foundation users
		owner = await createOwner();
		testUser = await createMember();

		// Get projects
		testUserPersonalProject = await getPersonalProject(testUser);
		teamProject = await createTeamProject('Access Control Test Project', owner);

		// Create authentication agents
		ownerAgent = testServer.authAgentFor(owner);
		testUserAgent = testServer.authAgentFor(testUser);

		await utils.initCredentialsTypes();

		// Create custom roles with specific scopes
		workflowReadOnlyRole = await createCustomRoleWithScopeSlugs(
			['workflow:read', 'workflow:list'],
			{
				roleType: 'project',
				displayName: 'Workflow Read-Only',
				description: 'Can only read and list workflows',
			},
		);

		workflowAllOperationsRole = await createCustomRoleWithScopeSlugs(
			[
				'workflow:share',
				'workflow:execute',
				'workflow:read',
				'workflow:list',
				'workflow:create',
				'workflow:update',
				'workflow:delete',
			],
			{
				roleType: 'project',
				displayName: 'Workflow All Operations',
				description: 'Full workflow access (CRUD + list)',
			},
		);

		credentialReadOnlyRole = await createCustomRoleWithScopeSlugs(
			['credential:read', 'credential:list'],
			{
				roleType: 'project',
				displayName: 'Credential Read-Only',
				description: 'Can only read and list credentials',
			},
		);

		credentialAllOperationsRole = await createCustomRoleWithScopeSlugs(
			[
				'credential:read',
				'credential:list',
				'credential:create',
				'credential:update',
				'credential:delete',
			],
			{
				roleType: 'project',
				displayName: 'Credential All Operations',
				description: 'Full credential access (CRUD + list)',
			},
		);
	});

	beforeEach(async () => {
		// Clean up any existing shared resources
		await testDb.truncate(['SharedWorkflow', 'SharedCredentials', 'ProjectRelation']);

		// Create fresh test data for each test
		const workflow = await createWorkflow({ name: 'Matrix Test Workflow' }, teamProject);
		testWorkflowId = workflow.id;
		testWorkflowVersionId = workflow.versionId;

		// Create test credential via owner
		const credentialPayload = randomCredentialPayload();
		const credentialResponse = await ownerAgent
			.post('/credentials')
			.send({ ...credentialPayload, projectId: teamProject.id })
			.expect(200);
		testCredentialId = credentialResponse.body.data.id;
	});

	afterAll(async () => {
		await testDb.truncate(['User', 'ProjectRelation']);
		await cleanupRolesAndScopes();
	});

	describe('Foundation Setup Validation', () => {
		test('should have created all required custom roles', () => {
			expect(workflowReadOnlyRole.scopes).toHaveLength(2);
			expect(workflowAllOperationsRole.scopes).toHaveLength(7);

			expect(credentialReadOnlyRole.scopes).toHaveLength(2);
			expect(credentialAllOperationsRole.scopes).toHaveLength(5);
		});

		test('should have functional test setup', async () => {
			expect(testWorkflowId).toBeDefined();
			expect(testCredentialId).toBeDefined();
			expect(teamProject).toBeDefined();

			// Verify owner can access test data
			await ownerAgent.get(`/workflows/${testWorkflowId}`).expect(200);
			await ownerAgent.get(`/credentials/${testCredentialId}`).expect(200);
		});
	});

	describe('Workflow Access Control Matrix', () => {
		describe('Workflow Read-Only Role', () => {
			beforeEach(async () => {
				await linkUserToProject(testUser, teamProject, workflowReadOnlyRole.slug);
			});

			test('POST /workflows should return 400', async () => {
				const workflowPayload = {
					name: 'New Workflow',
					active: false,
					nodes: [
						{
							id: 'uuid-1234',
							parameters: {},
							name: 'Start',
							type: 'n8n-nodes-base.manualTrigger',
							typeVersion: 1,
							position: [240, 300],
						},
					],
					connections: {},
					projectId: teamProject.id,
				};

				await testUserAgent.post('/workflows').send(workflowPayload).expect(400);
			});

			test('GET /workflows should return 200', async () => {
				const response = await testUserAgent.get('/workflows').expect(200);
				expect(Array.isArray(response.body.data)).toBe(true);
				expect(response.body.data.length).toBeGreaterThan(0);
			});

			test('GET /workflows/new should return 403', async () => {
				await testUserAgent.get(`/workflows/new?projectId=${teamProject.id}`).expect(403);
			});

			test('GET /workflows/:id should return 200', async () => {
				const response = await testUserAgent.get(`/workflows/${testWorkflowId}`).expect(200);
				expect(response.body.data.name).toBe('Matrix Test Workflow');
			});

			test('PATCH /workflows/:id should return 403', async () => {
				await testUserAgent
					.patch(`/workflows/${testWorkflowId}`)
					.send({ name: 'Updated Name', versionId: testWorkflowVersionId })
					.expect(403);
			});

			test('DELETE /workflows/:id should return 403', async () => {
				await testUserAgent.delete(`/workflows/${testWorkflowId}`).expect(403);
			});

			test('POST /workflows/:id/archive should return 403', async () => {
				await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(403);
			});

			test('POST /workflows/:id/unarchive should return 403', async () => {
				await testUserAgent.post(`/workflows/${testWorkflowId}/unarchive`).send().expect(403);
			});

			test('POST /workflows/:id/run should return 403', async () => {
				const runPayload = {
					workflowData: { id: testWorkflowId, name: 'Test', nodes: [], connections: {} },
				};
				await testUserAgent.post(`/workflows/${testWorkflowId}/run`).send(runPayload).expect(403);
			});

			test('PUT /workflows/:id/share should return 403', async () => {
				await testUserAgent
					.put(`/workflows/${testWorkflowId}/share`)
					.send({ shareWithIds: [testUserPersonalProject.id] })
					.expect(403);
			});

			test('PUT /workflows/:id/transfer should return 403', async () => {
				await testUserAgent
					.put(`/workflows/${testWorkflowId}/transfer`)
					.send({ destinationProjectId: testUserPersonalProject.id })
					.expect(403);
			});
		});

		describe('Workflow All-Operations Role', () => {
			beforeEach(async () => {
				await linkUserToProject(testUser, teamProject, workflowAllOperationsRole.slug);
			});

			test('POST /workflows should return 200', async () => {
				const workflowPayload = {
					name: 'All-Ops Workflow',
					active: false,
					nodes: [
						{
							id: 'uuid-1234',
							parameters: {},
							name: 'Start',
							type: 'n8n-nodes-base.manualTrigger',
							typeVersion: 1,
							position: [240, 300],
						},
					],
					connections: {},
					projectId: teamProject.id,
				};

				const response = await testUserAgent.post('/workflows').send(workflowPayload).expect(200);
				expect(response.body.data.name).toBe('All-Ops Workflow');
			});

			test('GET /workflows should return 200', async () => {
				const response = await testUserAgent.get('/workflows').expect(200);
				expect(Array.isArray(response.body.data)).toBe(true);
				expect(response.body.data.length).toBeGreaterThan(0);
			});

			test('GET /workflows/new should return 200', async () => {
				await testUserAgent.get(`/workflows/new?projectId=${teamProject.id}`).expect(200);
			});

			test('GET /workflows/:id should return 200', async () => {
				const response = await testUserAgent.get(`/workflows/${testWorkflowId}`).expect(200);
				expect(response.body.data.name).toBe('Matrix Test Workflow');
			});

			test('PATCH /workflows/:id should return 200', async () => {
				const response = await testUserAgent
					.patch(`/workflows/${testWorkflowId}`)
					.send({ name: 'Updated by All-Ops', versionId: testWorkflowVersionId })
					.expect(200);
				expect(response.body.data.name).toBe('Updated by All-Ops');
			});

			test('POST /workflows/:id/archive should return 200', async () => {
				// All-operations role (includes workflow:delete) can successfully archive workflows
				// Archive operation is allowed when user has full CRUD permissions
				await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
			});

			test('DELETE /workflows/:id should return 200', async () => {
				await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
				await testUserAgent.delete(`/workflows/${testWorkflowId}`).expect(200);
			});

			test('POST /workflows/:id/unarchive should return 200', async () => {
				await testUserAgent.post(`/workflows/${testWorkflowId}/archive`).send().expect(200);
				await testUserAgent.post(`/workflows/${testWorkflowId}/unarchive`).send().expect(200);
			});

			test('PUT /workflows/:id/share should return 200', async () => {
				// Sharing requires workflow:share scope (not included in CRUD operations)
				await testUserAgent
					.put(`/workflows/${testWorkflowId}/share`)
					.send({ shareWithIds: [testUserPersonalProject.id] })
					.expect(200);
			});
		});
	});

	describe('Credential Access Control Matrix', () => {
		describe('Credential Read-Only Role', () => {
			beforeEach(async () => {
				await linkUserToProject(testUser, teamProject, credentialReadOnlyRole.slug);
			});

			test('GET /credentials should return 200', async () => {
				const response = await testUserAgent.get('/credentials').expect(200);
				expect(Array.isArray(response.body.data)).toBe(true);
				expect(response.body.data.length).toBeGreaterThan(0);
			});

			test('GET /credentials/new should return 200', async () => {
				await testUserAgent.get('/credentials/new').expect(200);
			});

			test('GET /credentials/:id should return 200', async () => {
				const response = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);
				expect(response.body.data).toBeDefined();
			});

			test('POST /credentials should return 400', async () => {
				const credentialPayload = randomCredentialPayload();
				await testUserAgent
					.post('/credentials')
					.send({ ...credentialPayload, projectId: teamProject.id })
					.expect(400);
			});

			test('PATCH /credentials/:id should return 403', async () => {
				const updatePayload = { name: 'Updated Credential Name' };
				await testUserAgent
					.patch(`/credentials/${testCredentialId}`)
					.send(updatePayload)
					.expect(403);
			});

			test('DELETE /credentials/:id should return 403', async () => {
				await testUserAgent.delete(`/credentials/${testCredentialId}`).expect(403);
			});

			test('PUT /credentials/:id/share should return 403', async () => {
				await testUserAgent
					.put(`/credentials/${testCredentialId}/share`)
					.send({ shareWithIds: [testUserPersonalProject.id] })
					.expect(403);
			});

			test('PUT /credentials/:id/transfer should return 403', async () => {
				await testUserAgent
					.put(`/credentials/${testCredentialId}/transfer`)
					.send({ destinationProjectId: testUserPersonalProject.id })
					.expect(403);
			});
		});

		describe('Credential All-Operations Role', () => {
			beforeEach(async () => {
				await linkUserToProject(testUser, teamProject, credentialAllOperationsRole.slug);
			});

			test('GET /credentials should return 200', async () => {
				const response = await testUserAgent.get('/credentials').expect(200);
				expect(Array.isArray(response.body.data)).toBe(true);
				expect(response.body.data.length).toBeGreaterThan(0);
			});

			test('GET /credentials/new should return 200', async () => {
				await testUserAgent.get('/credentials/new').expect(200);
			});

			test('GET /credentials/:id should return 200', async () => {
				const response = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);
				expect(response.body.data).toBeDefined();
			});

			test('POST /credentials should return 200', async () => {
				const credentialPayload = randomCredentialPayload();
				const response = await testUserAgent
					.post('/credentials')
					.send({ ...credentialPayload, projectId: teamProject.id })
					.expect(200);
				expect(response.body.data.name).toBe(credentialPayload.name);
			});

			test('PATCH /credentials/:id should return 200', async () => {
				const original = await testUserAgent.get(`/credentials/${testCredentialId}`).expect(200);

				const updatePayload = { ...original.body.data, name: 'Updated by All-Ops', data: {} };

				const response = await testUserAgent
					.patch(`/credentials/${testCredentialId}`)
					.send(updatePayload)
					.expect(200);

				expect(response.body.data.name).toBe('Updated by All-Ops');
			});

			test('DELETE /credentials/:id should return 200', async () => {
				await testUserAgent.delete(`/credentials/${testCredentialId}`).expect(200);
			});

			test('PUT /credentials/:id/share should return 403', async () => {
				// Sharing requires credential:share scope (not included in CRUD operations)
				await testUserAgent
					.put(`/credentials/${testCredentialId}/share`)
					.send({ shareWithIds: [testUserPersonalProject.id] })
					.expect(403);
			});

			test('PUT /credentials/:id/transfer should return 403', async () => {
				// Transfer requires credential:move scope (not included in CRUD operations)
				await testUserAgent
					.put(`/credentials/${testCredentialId}/transfer`)
					.send({ destinationProjectId: testUserPersonalProject.id })
					.expect(403);
			});
		});
	});
});
