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

// Mock query functions
const mockQuery = jest.fn();
const mockQueryOne = jest.fn();

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

// Import after mocking
import { timesheetsService, Timesheet } from '../timesheets.service.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';

describe('TimesheetsService', () => {
  const tenantId = 'test-tenant-uuid';
  const userId = 'test-user-uuid';
  const companyId = 'test-company-uuid';

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('should return timesheets with pagination', async () => {
      const mockTimesheets = [
        createMockTimesheet({ id: '1', hours: 4 }),
        createMockTimesheet({ id: '2', hours: 8 }),
      ];

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

      const result = await timesheetsService.findAll(tenantId, { page: 1, limit: 20 });

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

    it('should filter by project_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.findAll(tenantId, { project_id: 'project-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.project_id = $'),
        expect.arrayContaining([tenantId, 'project-uuid'])
      );
    });

    it('should filter by task_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.findAll(tenantId, { task_id: 'task-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.task_id = $'),
        expect.arrayContaining([tenantId, 'task-uuid'])
      );
    });

    it('should filter by user_id', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.findAll(tenantId, { user_id: userId });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.user_id = $'),
        expect.arrayContaining([tenantId, userId])
      );
    });

    it('should filter by date range', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.findAll(tenantId, {
        date_from: '2026-01-01',
        date_to: '2026-01-31'
      });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.date >= $'),
        expect.arrayContaining([tenantId, '2026-01-01', '2026-01-31'])
      );
    });

    it('should filter by status', async () => {
      mockQueryOne.mockResolvedValue({ count: '0' });
      mockQuery.mockResolvedValue([]);

      await timesheetsService.findAll(tenantId, { status: 'approved' });

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

  describe('findById', () => {
    it('should return timesheet when found', async () => {
      const mockTimesheet = createMockTimesheet();
      mockQueryOne.mockResolvedValue(mockTimesheet);

      const result = await timesheetsService.findById('timesheet-uuid-1', tenantId);

      expect(result).toEqual(mockTimesheet);
    });

    it('should throw NotFoundError when not found', async () => {
      mockQueryOne.mockResolvedValue(null);

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

  describe('create', () => {
    const createDto = {
      company_id: companyId,
      project_id: 'project-uuid',
      task_id: 'task-uuid',
      date: '2026-01-15',
      hours: 8,
      description: 'Development work',
      billable: true,
    };

    it('should create timesheet successfully', async () => {
      const createdTimesheet = createMockTimesheet({ ...createDto });
      mockQueryOne.mockResolvedValue(createdTimesheet);

      const result = await timesheetsService.create(createDto, tenantId, userId);

      expect(result.hours).toBe(8);
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO projects.timesheets'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when hours is 0 or negative', async () => {
      await expect(
        timesheetsService.create({ ...createDto, hours: 0 }, tenantId, userId)
      ).rejects.toThrow(ValidationError);

      await expect(
        timesheetsService.create({ ...createDto, hours: -1 }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when hours exceeds 24', async () => {
      await expect(
        timesheetsService.create({ ...createDto, hours: 25 }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should create timesheet without task_id', async () => {
      const dtoWithoutTask = { ...createDto, task_id: undefined };
      const createdTimesheet = createMockTimesheet({ ...dtoWithoutTask, task_id: null });
      mockQueryOne.mockResolvedValue(createdTimesheet);

      const result = await timesheetsService.create(dtoWithoutTask, tenantId, userId);

      expect(result).toBeDefined();
    });

    it('should default billable to true', async () => {
      const dtoWithoutBillable = { ...createDto, billable: undefined };
      const createdTimesheet = createMockTimesheet({ ...dtoWithoutBillable, billable: true });
      mockQueryOne.mockResolvedValue(createdTimesheet);

      const result = await timesheetsService.create(dtoWithoutBillable, tenantId, userId);

      expect(result.billable).toBe(true);
    });
  });

  describe('update', () => {
    it('should update timesheet successfully when in draft status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne
        .mockResolvedValueOnce(existingTimesheet) // findById
        .mockResolvedValueOnce({ ...existingTimesheet, hours: 8 }); // after update
      mockQuery.mockResolvedValue([]);

      await timesheetsService.update('timesheet-uuid-1', { hours: 8 }, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE projects.timesheets SET'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when timesheet is not in draft status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'submitted',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

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

    it('should throw ValidationError when user is not the owner', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'other-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

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

    it('should throw ValidationError when updating hours outside valid range', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

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

    it('should return unchanged timesheet when no fields to update', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      const result = await timesheetsService.update('timesheet-uuid-1', {}, tenantId, userId);

      expect(result.id).toBe(existingTimesheet.id);
    });
  });

  describe('delete', () => {
    it('should delete timesheet when in draft status and owned by user', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);
      mockQuery.mockResolvedValue([]);

      await timesheetsService.delete('timesheet-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM projects.timesheets'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when timesheet is not in draft status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'approved',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.delete('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when user is not the owner', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'other-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.delete('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

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

      await expect(
        timesheetsService.delete('nonexistent-id', tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });
  });

  describe('submit', () => {
    it('should submit timesheet successfully', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne
        .mockResolvedValueOnce(existingTimesheet) // findById
        .mockResolvedValueOnce({ ...existingTimesheet, status: 'submitted' }); // after update
      mockQuery.mockResolvedValue([]);

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

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

    it('should throw ValidationError when not in draft status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: userId,
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'submitted',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.submit('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when user is not the owner', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'other-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.submit('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('approve', () => {
    it('should approve timesheet successfully', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'worker-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'submitted',
        created_at: new Date(),
      };
      const approverId = 'manager-uuid';
      mockQueryOne
        .mockResolvedValueOnce(existingTimesheet) // findById
        .mockResolvedValueOnce({ ...existingTimesheet, status: 'approved' }); // after update
      mockQuery.mockResolvedValue([]);

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

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

    it('should throw ValidationError when not in submitted status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'worker-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'draft',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.approve('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('reject', () => {
    it('should reject timesheet successfully', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'worker-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'submitted',
        created_at: new Date(),
      };
      const approverId = 'manager-uuid';
      mockQueryOne
        .mockResolvedValueOnce(existingTimesheet) // findById
        .mockResolvedValueOnce({ ...existingTimesheet, status: 'rejected' }); // after update
      mockQuery.mockResolvedValue([]);

      const result = await timesheetsService.reject('timesheet-uuid-1', tenantId, approverId);

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

    it('should throw ValidationError when not in submitted status', async () => {
      const existingTimesheet: Timesheet = {
        id: 'timesheet-uuid-1',
        tenant_id: tenantId,
        company_id: companyId,
        project_id: 'project-uuid',
        user_id: 'worker-user-uuid',
        date: new Date('2026-01-15'),
        hours: 4,
        billable: true,
        status: 'approved',
        created_at: new Date(),
      };
      mockQueryOne.mockResolvedValue(existingTimesheet);

      await expect(
        timesheetsService.reject('timesheet-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('getMyTimesheets', () => {
    it('should return timesheets for current user', async () => {
      const mockTimesheets = [
        createMockTimesheet({ user_id: userId }),
      ];

      mockQueryOne.mockResolvedValue({ count: '1' });
      mockQuery.mockResolvedValue(mockTimesheets);

      const result = await timesheetsService.getMyTimesheets(tenantId, userId, {});

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.user_id = $'),
        expect.arrayContaining([tenantId, userId])
      );
      expect(result.data).toHaveLength(1);
    });
  });

  describe('getPendingApprovals', () => {
    it('should return submitted timesheets pending approval', async () => {
      const mockTimesheets = [
        createMockTimesheet({ status: 'submitted' }),
      ];

      mockQueryOne.mockResolvedValue({ count: '1' });
      mockQuery.mockResolvedValue(mockTimesheets);

      const result = await timesheetsService.getPendingApprovals(tenantId, {});

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('ts.status = $'),
        expect.arrayContaining([tenantId, 'submitted'])
      );
      expect(result.data).toHaveLength(1);
    });
  });
});
