const request = require('supertest');
const app = require('../../app');
const { Project, User, Area, Task, Note } = require('../../models');
const { createTestUser } = require('../helpers/testUtils');

describe('Projects Routes', () => {
    let user, area, agent;

    beforeEach(async () => {
        user = await createTestUser({
            email: 'test@example.com',
        });

        area = await Area.create({
            name: 'Work',
            user_id: user.id,
        });

        // Create authenticated agent
        agent = request.agent(app);
        await agent.post('/api/login').send({
            email: 'test@example.com',
            password: 'password123',
        });
    });

    describe('POST /api/project', () => {
        it('should create a new project', async () => {
            const projectData = {
                name: 'Test Project',
                description: 'Test Description',
                status: 'planned',
                pin_to_sidebar: false,
                priority: 1,
                area_id: area.id,
            };

            const response = await agent.post('/api/project').send(projectData);

            expect(response.status).toBe(201);
            expect(response.body.name).toBe(projectData.name);
            expect(response.body.description).toBe(projectData.description);
            expect(response.body.status).toBe(projectData.status);
            expect(response.body.pin_to_sidebar).toBe(
                projectData.pin_to_sidebar
            );
            expect(response.body.priority).toBe(projectData.priority);
            expect(response.body.area_id).toBe(area.id);
            expect(response.body.user_id).toBe(user.id);
        });

        it('should require authentication', async () => {
            const projectData = {
                name: 'Test Project',
            };

            const response = await request(app)
                .post('/api/project')
                .send(projectData);

            expect(response.status).toBe(401);
            expect(response.body.error).toBe('Authentication required');
        });

        it('should require project name', async () => {
            const projectData = {
                description: 'Project without name',
            };

            const response = await agent.post('/api/project').send(projectData);

            expect(response.status).toBe(400);
        });
    });

    describe('GET /api/projects', () => {
        let project1, project2;

        beforeEach(async () => {
            project1 = await Project.create({
                name: 'Project 1',
                description: 'First project',
                user_id: user.id,
                area_id: area.id,
            });

            project2 = await Project.create({
                name: 'Project 2',
                description: 'Second project',
                user_id: user.id,
            });
        });

        it('should get all user projects', async () => {
            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            expect(response.body.projects).toBeDefined();
            expect(response.body.projects.length).toBe(2);
            expect(response.body.projects.map((p) => p.id)).toContain(
                project1.id
            );
            expect(response.body.projects.map((p) => p.id)).toContain(
                project2.id
            );
        });

        it('should include area information', async () => {
            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const projectWithArea = response.body.projects.find(
                (p) => p.id === project1.id
            );
            expect(projectWithArea.Area).toBeDefined();
            expect(projectWithArea.Area.name).toBe(area.name);
        });

        it('should require authentication', async () => {
            const response = await request(app).get('/api/projects');

            expect(response.status).toBe(401);
            expect(response.body.error).toBe('Authentication required');
        });
    });

    describe('GET /api/project/:id', () => {
        let project;

        beforeEach(async () => {
            project = await Project.create({
                name: 'Test Project',
                description: 'Test Description',
                user_id: user.id,
                area_id: area.id,
            });
        });

        it('should get project by uid-slug format', async () => {
            // Create a slug from the project UID and name
            const sluggedName = project.name.toLowerCase().replace(/\s+/g, '-');
            const uidSlug = `${project.uid}-${sluggedName}`;

            const response = await agent.get(`/api/project/${uidSlug}`);

            expect(response.status).toBe(200);
            expect(response.body.id).toBe(project.id);
            expect(response.body.name).toBe(project.name);
            expect(response.body.description).toBe(project.description);
        });

        it('should return 404 for non-existent project', async () => {
            const response = await agent.get(
                '/api/project/nonexistent-uid-slug'
            );

            expect(response.status).toBe(404);
            expect(response.body.error).toBe('Project not found');
        });

        it("should not allow access to other user's projects", async () => {
            const bcrypt = require('bcrypt');
            const otherUser = await User.create({
                email: 'other@example.com',
                password_digest: await bcrypt.hash('password123', 10),
            });

            const otherProject = await Project.create({
                name: 'Other Project',
                user_id: otherUser.id,
            });

            const response = await agent.get(`/api/project/${otherProject.id}`);

            expect(response.status).toBe(404);
            expect(response.body.error).toBe('Project not found');
        });

        it("should return 403 for other user's projects when accessed by uid-slug", async () => {
            const bcrypt = require('bcrypt');
            const otherUser = await User.create({
                email: 'other2@example.com',
                password_digest: await bcrypt.hash('password123', 10),
            });

            const otherProject = await Project.create({
                name: 'Secret Project',
                user_id: otherUser.id,
            });

            // Build proper uid-slug
            const sluggedName = otherProject.name
                .toLowerCase()
                .replace(/\s+/g, '-');
            const uidSlug = `${otherProject.uid}-${sluggedName}`;

            const response = await agent.get(`/api/project/${uidSlug}`);

            expect(response.status).toBe(403);
            expect(response.body.error).toBe('Forbidden');
        });

        it('should require authentication', async () => {
            const response = await request(app).get(
                `/api/project/${project.id}`
            );

            expect(response.status).toBe(401);
            expect(response.body.error).toBe('Authentication required');
        });
    });

    describe('PATCH /api/project/:id', () => {
        let project;

        beforeEach(async () => {
            project = await Project.create({
                name: 'Test Project',
                description: 'Test Description',
                status: 'not_started',
                priority: 0,
                user_id: user.id,
            });
        });

        it('should update project', async () => {
            const updateData = {
                name: 'Updated Project',
                description: 'Updated Description',
                status: 'in_progress',
                priority: 2,
            };

            const response = await agent
                .patch(`/api/project/${project.uid}`)
                .send(updateData);

            expect(response.status).toBe(200);
            expect(response.body.name).toBe(updateData.name);
            expect(response.body.description).toBe(updateData.description);
            expect(response.body.status).toBe(updateData.status);
            expect(response.body.priority).toBe(updateData.priority);
        });

        it('should return 404 for non-existent project', async () => {
            const response = await agent
                .patch('/api/project/nonexistentuid')
                .send({ name: 'Updated' });

            expect(response.status).toBe(404);
            expect(response.body.error).toBe('Project not found.');
        });

        it("should not allow updating other user's projects", async () => {
            const bcrypt = require('bcrypt');
            const otherUser = await User.create({
                email: 'other@example.com',
                password_digest: await bcrypt.hash('password123', 10),
            });

            const otherProject = await Project.create({
                name: 'Other Project',
                user_id: otherUser.id,
            });

            const response = await agent
                .patch(`/api/project/${otherProject.uid}`)
                .send({ name: 'Updated' });

            expect(response.status).toBe(403);
            expect(response.body.error).toBe('Forbidden');
        });

        it('should require authentication', async () => {
            const response = await request(app)
                .patch(`/api/project/${project.uid}`)
                .send({ name: 'Updated' });

            expect(response.status).toBe(401);
            expect(response.body.error).toBe('Authentication required');
        });
    });

    describe('DELETE /api/project/:id', () => {
        let project;

        beforeEach(async () => {
            project = await Project.create({
                name: 'Test Project',
                user_id: user.id,
            });
        });

        it('should delete project', async () => {
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();
        });

        it('should return 404 for non-existent project', async () => {
            const response = await agent.delete('/api/project/nonexistentuid');

            expect(response.status).toBe(404);
            expect(response.body.error).toBe('Project not found.');
        });

        it("should not allow deleting other user's projects", async () => {
            const bcrypt = require('bcrypt');
            const otherUser = await User.create({
                email: 'other@example.com',
                password_digest: await bcrypt.hash('password123', 10),
            });

            const otherProject = await Project.create({
                name: 'Other Project',
                user_id: otherUser.id,
            });

            const response = await agent.delete(
                `/api/project/${otherProject.uid}`
            );

            expect(response.status).toBe(403);
            expect(response.body.error).toBe('Forbidden');
        });

        it('should require authentication', async () => {
            const response = await request(app).delete(
                `/api/project/${project.uid}`
            );

            expect(response.status).toBe(401);
            expect(response.body.error).toBe('Authentication required');
        });

        it('should delete project with associated tasks (orphan tasks)', async () => {
            // Create tasks associated with the project
            const task1 = await Task.create({
                name: 'Task 1',
                user_id: user.id,
                project_id: project.id,
                status: 0, // not_started
            });

            const task2 = await Task.create({
                name: 'Task 2',
                user_id: user.id,
                project_id: project.id,
                status: 2, // done/completed
            });

            // Delete the project
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();

            // Verify tasks are orphaned (project_id set to null) but still exist
            const orphanedTask1 = await Task.findByPk(task1.id);
            const orphanedTask2 = await Task.findByPk(task2.id);

            expect(orphanedTask1).not.toBeNull();
            expect(orphanedTask1.project_id).toBeNull();
            expect(orphanedTask1.name).toBe('Task 1');

            expect(orphanedTask2).not.toBeNull();
            expect(orphanedTask2.project_id).toBeNull();
            expect(orphanedTask2.name).toBe('Task 2');
        });

        it('should delete project with completed tasks only', async () => {
            // Create only completed tasks associated with the project
            const completedTask = await Task.create({
                name: 'Completed Task',
                user_id: user.id,
                project_id: project.id,
                status: 2, // done/completed
            });

            // Delete the project
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();

            // Verify completed task is orphaned but still exists
            const orphanedTask = await Task.findByPk(completedTask.id);
            expect(orphanedTask).not.toBeNull();
            expect(orphanedTask.project_id).toBeNull();
            expect(orphanedTask.status).toBe(2); // Still completed
        });

        it('should delete project with mixed status tasks', async () => {
            // Create tasks with different statuses
            const notStartedTask = await Task.create({
                name: 'Not Started Task',
                user_id: user.id,
                project_id: project.id,
                status: 0, // not_started
            });

            const inProgressTask = await Task.create({
                name: 'In Progress Task',
                user_id: user.id,
                project_id: project.id,
                status: 1, // in_progress
            });

            const completedTask = await Task.create({
                name: 'Completed Task',
                user_id: user.id,
                project_id: project.id,
                status: 2, // done/completed
            });

            // Delete the project
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();

            // Verify all tasks are orphaned but still exist with their original statuses
            const tasks = await Task.findAll({
                where: {
                    id: [
                        notStartedTask.id,
                        inProgressTask.id,
                        completedTask.id,
                    ],
                },
            });

            expect(tasks).toHaveLength(3);

            const taskById = {};
            tasks.forEach((task) => {
                taskById[task.id] = task;
                expect(task.project_id).toBeNull(); // All should be orphaned
            });

            expect(taskById[notStartedTask.id].status).toBe(0);
            expect(taskById[inProgressTask.id].status).toBe(1);
            expect(taskById[completedTask.id].status).toBe(2);
        });

        it('should delete project with associated notes (orphan notes)', async () => {
            // Create notes associated with the project
            const note1 = await Note.create({
                title: 'Note 1',
                content: 'Content for note 1',
                user_id: user.id,
                project_id: project.id,
            });

            const note2 = await Note.create({
                title: 'Note 2',
                content: 'Content for note 2',
                user_id: user.id,
                project_id: project.id,
            });

            const note3 = await Note.create({
                title: 'Note 3',
                content: 'Content for note 3',
                user_id: user.id,
                project_id: project.id,
            });

            // Delete the project
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();

            // Verify notes are orphaned (project_id set to null) but still exist
            const orphanedNote1 = await Note.findByPk(note1.id);
            const orphanedNote2 = await Note.findByPk(note2.id);
            const orphanedNote3 = await Note.findByPk(note3.id);

            expect(orphanedNote1).not.toBeNull();
            expect(orphanedNote1.project_id).toBeNull();
            expect(orphanedNote1.title).toBe('Note 1');
            expect(orphanedNote1.content).toBe('Content for note 1');

            expect(orphanedNote2).not.toBeNull();
            expect(orphanedNote2.project_id).toBeNull();
            expect(orphanedNote2.title).toBe('Note 2');

            expect(orphanedNote3).not.toBeNull();
            expect(orphanedNote3.project_id).toBeNull();
            expect(orphanedNote3.title).toBe('Note 3');
        });

        it('should delete project with both tasks and notes (orphan both)', async () => {
            // Create tasks associated with the project
            const task = await Task.create({
                name: 'Task with project',
                user_id: user.id,
                project_id: project.id,
                status: 0,
            });

            // Create notes associated with the project
            const note = await Note.create({
                title: 'Note with project',
                content: 'This note belongs to a project',
                user_id: user.id,
                project_id: project.id,
            });

            // Delete the project
            const response = await agent.delete(`/api/project/${project.uid}`);

            expect(response.status).toBe(200);
            expect(response.body.message).toBe('Project successfully deleted');

            // Verify project is deleted
            const deletedProject = await Project.findByPk(project.id);
            expect(deletedProject).toBeNull();

            // Verify task is orphaned but still exists
            const orphanedTask = await Task.findByPk(task.id);
            expect(orphanedTask).not.toBeNull();
            expect(orphanedTask.project_id).toBeNull();
            expect(orphanedTask.name).toBe('Task with project');

            // Verify note is orphaned but still exists
            const orphanedNote = await Note.findByPk(note.id);
            expect(orphanedNote).not.toBeNull();
            expect(orphanedNote.project_id).toBeNull();
            expect(orphanedNote.title).toBe('Note with project');
            expect(orphanedNote.content).toBe('This note belongs to a project');
        });
    });

    describe('GET /api/projects - is_stalled flag', () => {
        it('should mark in_progress project with no tasks as stalled', async () => {
            await Project.create({
                name: 'Empty In Progress Project',
                status: 'in_progress',
                user_id: user.id,
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const project = response.body.projects.find(
                (p) => p.name === 'Empty In Progress Project'
            );
            expect(project.is_stalled).toBe(true);
        });

        it('should mark planned project with no tasks as stalled', async () => {
            await Project.create({
                name: 'Empty Planned Project',
                status: 'planned',
                user_id: user.id,
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const project = response.body.projects.find(
                (p) => p.name === 'Empty Planned Project'
            );
            expect(project.is_stalled).toBe(true);
        });

        it('should not mark in_progress project with active tasks as stalled', async () => {
            const project = await Project.create({
                name: 'Active Project',
                status: 'in_progress',
                user_id: user.id,
            });

            await Task.create({
                name: 'Active Task',
                user_id: user.id,
                project_id: project.id,
                status: 1, // in_progress
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const foundProject = response.body.projects.find(
                (p) => p.name === 'Active Project'
            );
            expect(foundProject.is_stalled).toBe(false);
        });

        it('should not mark in_progress project with not_started tasks as stalled', async () => {
            const project = await Project.create({
                name: 'Project With Pending Tasks',
                status: 'in_progress',
                user_id: user.id,
            });

            await Task.create({
                name: 'Pending Task',
                user_id: user.id,
                project_id: project.id,
                status: 0, // not_started
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const foundProject = response.body.projects.find(
                (p) => p.name === 'Project With Pending Tasks'
            );
            expect(foundProject.is_stalled).toBe(false);
        });

        it('should mark in_progress project with only completed tasks as stalled', async () => {
            const project = await Project.create({
                name: 'All Done Project',
                status: 'in_progress',
                user_id: user.id,
            });

            await Task.create({
                name: 'Completed Task',
                user_id: user.id,
                project_id: project.id,
                status: 2, // done
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const foundProject = response.body.projects.find(
                (p) => p.name === 'All Done Project'
            );
            expect(foundProject.is_stalled).toBe(true);
        });

        it('should not mark not_started project as stalled even with no tasks', async () => {
            await Project.create({
                name: 'Not Started Project',
                status: 'not_started',
                user_id: user.id,
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const project = response.body.projects.find(
                (p) => p.name === 'Not Started Project'
            );
            expect(project.is_stalled).toBe(false);
        });

        it('should not mark done project as stalled even with no active tasks', async () => {
            await Project.create({
                name: 'Completed Project',
                status: 'done',
                user_id: user.id,
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const project = response.body.projects.find(
                (p) => p.name === 'Completed Project'
            );
            expect(project.is_stalled).toBe(false);
        });

        it('should not mark waiting project as stalled even with no tasks', async () => {
            await Project.create({
                name: 'Waiting Project',
                status: 'waiting',
                user_id: user.id,
            });

            const response = await agent.get('/api/projects');

            expect(response.status).toBe(200);
            const project = response.body.projects.find(
                (p) => p.name === 'Waiting Project'
            );
            expect(project.is_stalled).toBe(false);
        });
    });
});
