import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import {
  createMockProject,
  createMockTask,
  createMockTimesheet,
} from '../../../__tests__/helpers.js';

// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();
const mockGetClient = jest.fn();
const mockClient = {
  query: jest.fn(),
  release: jest.fn(),
};

jest.mock('../../../config/database.js', () => ({
  query: (...args: any[]) => mockQuery(...args),
  queryOne: (...args: any[]) => mockQueryOne(...args),
  getClient: () => mockGetClient(),
}));

// Import after mocking
import { projectsService } from '../projects.service.js';
import { tasksService } from '../tasks.service.js';
import { timesheetsService } from '../timesheets.service.js';
import { billingService } from '../billing.service.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';

describe('Projects Module Integration Tests', () => {
  const tenantId = 'test-tenant-uuid';
  const userId = 'test-user-uuid';
  const companyId = 'test-company-uuid';
  const managerId = 'manager-uuid';
  const partnerId = 'partner-uuid';

  beforeEach(() => {
    jest.clearAllMocks();
    mockGetClient.mockResolvedValue(mockClient);
    mockClient.query.mockReset();
    mockClient.release.mockReset();
  });

  describe('Flow 1: Project Creation -> Tasks -> Resource Assignment', () => {
    it('should create a complete project workflow', async () => {
      // Step 1: Create project
      const projectData = {
        company_id: companyId,
        name: 'Website Redesign',
        code: 'WEB-001',
        description: 'Complete website redesign project',
        manager_id: managerId,
        partner_id: partnerId,
        date_start: '2026-01-01',
        date_end: '2026-06-30',
        allow_timesheets: true,
      };

      const createdProject = createMockProject({
        ...projectData,
        id: 'new-project-uuid',
        status: 'draft',
      });

      mockQueryOne
        .mockResolvedValueOnce(null) // code uniqueness check
        .mockResolvedValueOnce(createdProject); // INSERT

      const project = await projectsService.create(projectData, tenantId, userId);

      expect(project.id).toBe('new-project-uuid');
      expect(project.name).toBe('Website Redesign');

      // Step 2: Update project status to active
      mockQueryOne.mockResolvedValue({ ...createdProject, status: 'active' });
      mockQuery.mockResolvedValue([]);

      await projectsService.update('new-project-uuid', { status: 'active' }, tenantId, userId);

      // Step 3: Create tasks for the project
      const task1Data = {
        project_id: 'new-project-uuid',
        name: 'Design Phase',
        description: 'Create mockups and wireframes',
        estimated_hours: 40,
        priority: 'high' as const,
      };

      const createdTask1 = createMockTask({
        ...task1Data,
        id: 'task-1-uuid',
        sequence: 1,
      });

      mockQueryOne
        .mockResolvedValueOnce({ max_seq: 1 }) // sequence
        .mockResolvedValueOnce(createdTask1); // INSERT

      const task1 = await tasksService.create(task1Data, tenantId, userId);
      expect(task1.name).toBe('Design Phase');
      expect(task1.estimated_hours).toBe(40);

      // Step 4: Assign resource to task
      const assignedTask = createMockTask({
        ...createdTask1,
        assigned_to: 'designer-uuid',
        assigned_name: 'John Designer',
      });

      mockQueryOne.mockResolvedValue(createdTask1);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValue(assignedTask);

      const taskWithAssignment = await tasksService.assign('task-1-uuid', 'designer-uuid', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('assigned_to = $1'),
        expect.arrayContaining(['designer-uuid'])
      );
    });

    it('should prevent creating tasks for non-existent projects', async () => {
      const taskData = {
        project_id: 'nonexistent-project',
        name: 'Orphan Task',
      };

      // Simulate foreign key constraint - project query returns null
      mockQueryOne.mockResolvedValue({ max_seq: 1 });
      mockQueryOne.mockRejectedValue(new Error('foreign key constraint violation'));

      await expect(
        tasksService.create(taskData, tenantId, userId)
      ).rejects.toThrow();
    });
  });

  describe('Flow 2: Task -> Time Registration (Timesheet Entries)', () => {
    it('should complete timesheet entry workflow for a task', async () => {
      const projectId = 'project-uuid-1';
      const taskId = 'task-uuid-1';

      // Step 1: Create timesheet entry
      const timesheetData = {
        company_id: companyId,
        project_id: projectId,
        task_id: taskId,
        date: '2026-01-15',
        hours: 4,
        description: 'Worked on mockups',
        billable: true,
      };

      const createdTimesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: projectId,
        task_id: taskId,
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        description: 'Worked on mockups',
        billable: true,
        status: 'draft' as const,
        created_at: new Date(),
      };

      mockQueryOne.mockResolvedValue(createdTimesheet);

      const timesheet = await timesheetsService.create(timesheetData, tenantId, userId);

      expect(timesheet.hours).toBe(4);
      expect(timesheet.status).toBe('draft');

      // Step 2: Submit timesheet for approval
      mockQueryOne
        .mockResolvedValueOnce(createdTimesheet) // findById
        .mockResolvedValueOnce({ ...createdTimesheet, status: 'submitted' }); // after update
      mockQuery.mockResolvedValue([]);

      const submittedTimesheet = await timesheetsService.submit('timesheet-uuid-1', tenantId, userId);

      // Step 3: Approve timesheet (by manager)
      const submittedState = { ...createdTimesheet, status: 'submitted' as const };
      mockQueryOne
        .mockResolvedValueOnce(submittedState) // findById
        .mockResolvedValueOnce({ ...submittedState, status: 'approved' }); // after update
      mockQuery.mockResolvedValue([]);

      const approvedTimesheet = await timesheetsService.approve('timesheet-uuid-1', tenantId, managerId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status = 'approved'"),
        expect.any(Array)
      );
    });

    it('should track accumulated hours per task', async () => {
      const taskId = 'task-uuid-1';

      // Simulate multiple timesheet entries for a task
      const timesheets = [
        createMockTimesheet({ id: '1', task_id: taskId, hours: 4 }),
        createMockTimesheet({ id: '2', task_id: taskId, hours: 3 }),
        createMockTimesheet({ id: '3', task_id: taskId, hours: 2 }),
      ];

      mockQueryOne.mockResolvedValue({ count: '3' });
      mockQuery.mockResolvedValue(timesheets);

      const result = await timesheetsService.findAll(tenantId, { task_id: taskId });

      const totalHours = result.data.reduce((sum: number, ts: any) => sum + ts.hours, 0);
      expect(totalHours).toBe(9);
      expect(result.data).toHaveLength(3);
    });

    it('should validate daily hours limit', async () => {
      // Try to create timesheet with > 24 hours
      const invalidTimesheetData = {
        company_id: companyId,
        project_id: 'project-uuid',
        task_id: 'task-uuid',
        date: '2026-01-15',
        hours: 25, // Invalid
        description: 'Too many hours',
        billable: true,
      };

      await expect(
        timesheetsService.create(invalidTimesheetData, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('Flow 3: Project Progress Calculation', () => {
    it('should calculate project progress based on completed tasks', async () => {
      const projectId = 'project-uuid-1';

      const project = createMockProject({ id: projectId });
      const stats = {
        total_tasks: 10,
        completed_tasks: 6,
        in_progress_tasks: 3,
        total_hours: 120,
        total_milestones: 4,
        completed_milestones: 2,
      };

      mockQueryOne
        .mockResolvedValueOnce(project) // findById
        .mockResolvedValueOnce(stats); // getStats

      const projectStats = await projectsService.getStats(projectId, tenantId);

      expect(projectStats).toMatchObject({
        total_tasks: 10,
        completed_tasks: 6,
        completion_percentage: 60, // 6/10 * 100
        in_progress_tasks: 3,
        total_hours: 120,
      });
    });

    it('should handle project with no tasks (0% progress)', async () => {
      const projectId = 'empty-project-uuid';

      const project = createMockProject({ id: projectId });
      const stats = {
        total_tasks: 0,
        completed_tasks: 0,
        in_progress_tasks: 0,
        total_hours: 0,
        total_milestones: 0,
        completed_milestones: 0,
      };

      mockQueryOne
        .mockResolvedValueOnce(project)
        .mockResolvedValueOnce(stats);

      const projectStats = await projectsService.getStats(projectId, tenantId);

      expect((projectStats as any).completion_percentage).toBe(0);
    });

    it('should update task status and reflect in progress', async () => {
      const taskId = 'task-uuid-1';

      // Update task to done
      const existingTask = createMockTask({ id: taskId, status: 'in_progress' });
      mockQueryOne.mockResolvedValue(existingTask);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValue({ ...existingTask, status: 'done' });

      await tasksService.update(taskId, { status: 'done' }, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('status = $'),
        expect.arrayContaining(['done'])
      );
    });
  });

  describe('Flow 4: Project Billing (Time & Materials)', () => {
    it('should get billing rate for project with priority resolution', async () => {
      const projectId = 'project-uuid-1';
      const workerId = 'worker-uuid';

      // Mock billing rate query - should return most specific rate
      const billingRate = {
        id: 'rate-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: projectId,
        user_id: workerId,
        rate_type: 'project_user',
        hourly_rate: 150.00,
        currency_id: 'currency-mxn',
        currency_code: 'MXN',
        effective_from: null,
        effective_to: null,
        active: true,
        created_at: new Date(),
      };

      mockQuery.mockResolvedValue([billingRate]);

      const rate = await billingService.getBillingRate(
        tenantId,
        companyId,
        projectId,
        workerId,
        new Date()
      );

      expect(rate).toBeDefined();
      expect(rate?.hourly_rate).toBe(150.00);
      expect(rate?.rate_type).toBe('project_user');
    });

    it('should calculate unbilled timesheet amounts', async () => {
      const projectId = 'project-uuid-1';

      // Mock unbilled timesheets with billing amounts
      const unbilledTimesheets = [
        {
          id: 'ts-1',
          project_id: projectId,
          project_name: 'Test Project',
          user_id: 'worker-1',
          user_name: 'John Doe',
          date: new Date('2026-01-15'),
          hours: 8,
          description: 'Development',
          hourly_rate: 150.00,
          billable_amount: 1200.00,
          currency_id: 'curr-mxn',
          currency_code: 'MXN',
        },
        {
          id: 'ts-2',
          project_id: projectId,
          project_name: 'Test Project',
          user_id: 'worker-1',
          user_name: 'John Doe',
          date: new Date('2026-01-16'),
          hours: 6,
          description: 'Testing',
          hourly_rate: 150.00,
          billable_amount: 900.00,
          currency_id: 'curr-mxn',
          currency_code: 'MXN',
        },
      ];

      mockQueryOne.mockResolvedValue({ count: '2' });
      mockQuery.mockResolvedValue(unbilledTimesheets);

      const result = await billingService.getUnbilledTimesheets(tenantId, companyId, {
        project_id: projectId,
      });

      expect(result.data).toHaveLength(2);
      const totalBillable = result.data.reduce((sum, ts) => sum + ts.billable_amount, 0);
      expect(totalBillable).toBe(2100.00);
    });

    it('should create billing rate with validation', async () => {
      const createRateDto = {
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'worker-uuid',
        hourly_rate: 200.00,
        currency_id: 'currency-mxn',
      };

      const createdRate = {
        id: 'new-rate-uuid',
        tenant_id: tenantId,
        ...createRateDto,
        rate_type: 'project_user',
        active: true,
        created_at: new Date(),
      };

      mockQueryOne.mockResolvedValue(createdRate);

      const rate = await billingService.createBillingRate(createRateDto, tenantId, userId);

      expect(rate.hourly_rate).toBe(200.00);
      expect(rate.rate_type).toBe('project_user');
    });

    it('should reject negative hourly rates', async () => {
      const invalidRateDto = {
        company_id: companyId,
        hourly_rate: -50.00,
        currency_id: 'currency-mxn',
      };

      await expect(
        billingService.createBillingRate(invalidRateDto, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('Flow 5: Project Reports', () => {
    it('should generate billing summary by project', async () => {
      const summaryData = [
        {
          project_id: 'proj-1',
          project_name: 'Project Alpha',
          partner_id: 'partner-1',
          partner_name: 'Client A',
          total_hours: 80,
          total_amount: 12000.00,
          currency_id: 'curr-mxn',
          currency_code: 'MXN',
          timesheet_count: 20,
          date_from: new Date('2026-01-01'),
          date_to: new Date('2026-01-31'),
        },
        {
          project_id: 'proj-2',
          project_name: 'Project Beta',
          partner_id: 'partner-2',
          partner_name: 'Client B',
          total_hours: 120,
          total_amount: 18000.00,
          currency_id: 'curr-mxn',
          currency_code: 'MXN',
          timesheet_count: 30,
          date_from: new Date('2026-01-01'),
          date_to: new Date('2026-01-31'),
        },
      ];

      mockQuery.mockResolvedValue(summaryData);

      const summary = await billingService.getBillingSummary(tenantId, companyId, {});

      expect(summary).toHaveLength(2);
      expect(summary[0].total_hours).toBe(80);
      expect(summary[1].total_hours).toBe(120);

      const totalAmount = summary.reduce((sum, s) => sum + s.total_amount, 0);
      expect(totalAmount).toBe(30000.00);
    });

    it('should get project billing history', async () => {
      const projectId = 'project-uuid-1';

      // Mock billed stats
      const billedStats = { hours: '40', amount: '6000' };
      // Mock unbilled stats
      const unbilledStats = { hours: '20', amount: '3000' };
      // Mock invoices
      const invoices = [
        { id: 'inv-1', number: 'INV-001', date: new Date('2026-01-15'), amount: 3000, status: 'paid' },
        { id: 'inv-2', number: 'INV-002', date: new Date('2026-01-30'), amount: 3000, status: 'open' },
      ];

      mockQueryOne
        .mockResolvedValueOnce(billedStats)
        .mockResolvedValueOnce(unbilledStats);
      mockQuery.mockResolvedValue(invoices);

      const history = await billingService.getProjectBillingHistory(tenantId, projectId);

      expect(history.total_hours_billed).toBe(40);
      expect(history.total_amount_billed).toBe(6000);
      expect(history.unbilled_hours).toBe(20);
      expect(history.unbilled_amount).toBe(3000);
      expect(history.invoices).toHaveLength(2);
    });
  });

  describe('Flow 6: Task Hierarchy and Subtasks', () => {
    it('should create subtasks under parent task', async () => {
      const parentTaskId = 'parent-task-uuid';
      const subtaskData = {
        project_id: 'project-uuid',
        name: 'Subtask 1',
        parent_id: parentTaskId,
        estimated_hours: 4,
      };

      const createdSubtask = createMockTask({
        ...subtaskData,
        id: 'subtask-uuid',
        sequence: 1,
      });

      mockQueryOne
        .mockResolvedValueOnce({ max_seq: 1 })
        .mockResolvedValueOnce(createdSubtask);

      const subtask = await tasksService.create(subtaskData, tenantId, userId);

      expect(subtask.parent_id).toBe(parentTaskId);
    });

    it('should prevent task from being its own parent', async () => {
      const taskId = 'task-uuid-1';
      const existingTask = createMockTask({ id: taskId });

      mockQueryOne.mockResolvedValue(existingTask);

      await expect(
        tasksService.update(taskId, { parent_id: taskId }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('Flow 7: Task Stage Movement (Kanban)', () => {
    it('should move task between stages', async () => {
      const taskId = 'task-uuid-1';
      const newStageId = 'stage-in-progress-uuid';
      const newSequence = 3;

      const existingTask = createMockTask({
        id: taskId,
        stage_id: 'stage-todo-uuid',
        sequence: 1,
      });

      mockQueryOne.mockResolvedValue(existingTask);
      mockQuery.mockResolvedValue([]);
      mockQueryOne.mockResolvedValue({
        ...existingTask,
        stage_id: newStageId,
        sequence: newSequence,
      });

      const movedTask = await tasksService.move(taskId, newStageId, newSequence, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('stage_id = $1, sequence = $2'),
        expect.arrayContaining([newStageId, newSequence])
      );
    });
  });

  describe('Flow 8: Timesheet Approval Workflow', () => {
    it('should complete full approval workflow', async () => {
      const timesheetId = 'timesheet-uuid-1';
      const workerId = 'worker-uuid';

      // Initial draft state
      const draftTimesheet = {
        id: timesheetId,
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: workerId,
        date: new Date('2026-01-15'),
        hours: 8,
        billable: true,
        status: 'draft' as const,
        created_at: new Date(),
      };

      // Step 1: Submit by worker
      mockQueryOne
        .mockResolvedValueOnce(draftTimesheet)
        .mockResolvedValueOnce({ ...draftTimesheet, status: 'submitted' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.submit(timesheetId, tenantId, workerId);

      // Step 2: Reject by manager (needs changes)
      const submittedTimesheet = { ...draftTimesheet, status: 'submitted' as const };
      mockQueryOne
        .mockResolvedValueOnce(submittedTimesheet)
        .mockResolvedValueOnce({ ...submittedTimesheet, status: 'rejected' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.reject(timesheetId, tenantId, managerId);

      // Note: After rejection, worker would need to edit and resubmit
      // This simulates the workflow but actual re-editing requires
      // the status to be reset to draft first
    });

    it('should not allow editing approved timesheets', async () => {
      const approvedTimesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 8,
        billable: true,
        status: 'approved' as const,
        created_at: new Date(),
      };

      mockQueryOne.mockResolvedValue(approvedTimesheet);

      await expect(
        timesheetsService.update('timesheet-uuid-1', { hours: 10 }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('Edge Cases and Error Handling', () => {
    it('should handle project not found', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        projectsService.findById('nonexistent', tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should handle task not found', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        tasksService.findById('nonexistent', tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should handle timesheet not found', async () => {
      mockQueryOne.mockResolvedValue(null);

      await expect(
        timesheetsService.findById('nonexistent', tenantId)
      ).rejects.toThrow(NotFoundError);
    });

    it('should handle concurrent modifications gracefully', async () => {
      const project = createMockProject();

      // Simulate first read
      mockQueryOne.mockResolvedValue(project);
      mockQuery.mockResolvedValue([]);

      // Multiple concurrent updates should not throw
      await Promise.all([
        projectsService.update('project-uuid-1', { name: 'Update 1' }, tenantId, userId),
        projectsService.update('project-uuid-1', { name: 'Update 2' }, tenantId, 'other-user'),
      ]);

      expect(mockQuery).toHaveBeenCalledTimes(2);
    });

    it('should handle empty result sets', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      const result = await projectsService.findAll(tenantId, {
        status: 'nonexistent-status',
      });

      expect(result.data).toHaveLength(0);
      expect(result.total).toBe(0);
    });
  });

  describe('Time Calculations', () => {
    it('should calculate total time spent on project', async () => {
      const projectId = 'project-uuid-1';

      const project = createMockProject({ id: projectId });
      const stats = {
        total_tasks: 5,
        completed_tasks: 3,
        in_progress_tasks: 2,
        total_hours: 156.5, // Decimal hours
        total_milestones: 2,
        completed_milestones: 1,
      };

      mockQueryOne
        .mockResolvedValueOnce(project)
        .mockResolvedValueOnce(stats);

      const projectStats = await projectsService.getStats(projectId, tenantId);

      expect((projectStats as any).total_hours).toBe(156.5);
    });

    it('should track estimated vs actual hours per task', async () => {
      const task = createMockTask({
        id: 'task-uuid-1',
        estimated_hours: 20,
        spent_hours: 15,
      });

      mockQueryOne.mockResolvedValue(task);

      const result = await tasksService.findById('task-uuid-1', tenantId);

      expect(result.estimated_hours).toBe(20);
      expect(result.spent_hours).toBe(15);

      // Calculate remaining hours
      const remaining = (result.estimated_hours || 0) - (result.spent_hours || 0);
      expect(remaining).toBe(5);
    });
  });
});
