import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { createMockProject } 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 { projectsService } from '../projects.service.js';
import { NotFoundError, ConflictError } from '../../../shared/errors/index.js';

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

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

  describe('findAll', () => {
    it('should return projects with pagination', async () => {
      const mockProjects = [
        createMockProject({ id: '1', name: 'Project 1' }),
        createMockProject({ id: '2', name: 'Project 2' }),
      ];

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

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

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

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

      await projectsService.findAll(tenantId, { status: 'active' });

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

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

      await projectsService.findAll(tenantId, { manager_id: 'manager-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('p.manager_id = $'),
        expect.arrayContaining([tenantId, 'manager-uuid'])
      );
    });

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

      await projectsService.findAll(tenantId, { search: 'Test' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('p.name ILIKE'),
        expect.arrayContaining([tenantId, '%Test%'])
      );
    });
  });

  describe('findById', () => {
    it('should return project when found', async () => {
      const mockProject = createMockProject();
      mockQueryOne.mockResolvedValue(mockProject);

      const result = await projectsService.findById('project-uuid-1', tenantId);

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

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

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

  describe('create', () => {
    const createDto = {
      company_id: 'company-uuid',
      name: 'New Project',
      code: 'PROJ-001',
    };

    it('should create project successfully', async () => {
      const createdProject = createMockProject({ ...createDto });
      mockQueryOne
        .mockResolvedValueOnce(null) // unique code check
        .mockResolvedValueOnce(createdProject); // INSERT

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

      expect(result.name).toBe(createDto.name);
    });

    it('should throw ConflictError when code exists', async () => {
      mockQueryOne.mockResolvedValue({ id: 'existing-uuid' });

      await expect(
        projectsService.create(createDto, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should create project without code', async () => {
      const dtoWithoutCode = { company_id: 'company-uuid', name: 'Project' };
      const createdProject = createMockProject({ ...dtoWithoutCode, code: null });
      mockQueryOne.mockResolvedValue(createdProject);

      const result = await projectsService.create(dtoWithoutCode, tenantId, userId);

      expect(result.name).toBe(dtoWithoutCode.name);
    });
  });

  describe('update', () => {
    it('should update project successfully', async () => {
      const existingProject = createMockProject();
      mockQueryOne.mockResolvedValue(existingProject);
      mockQuery.mockResolvedValue([]);

      await projectsService.update(
        'project-uuid-1',
        { name: 'Updated Project' },
        tenantId,
        userId
      );

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

    it('should throw ConflictError when code exists for another project', async () => {
      const existingProject = createMockProject();
      mockQueryOne
        .mockResolvedValueOnce(existingProject) // findById
        .mockResolvedValueOnce({ id: 'other-uuid' }); // code exists

      await expect(
        projectsService.update('project-uuid-1', { code: 'DUPLICATE' }, tenantId, userId)
      ).rejects.toThrow(ConflictError);
    });

    it('should return unchanged project when no fields to update', async () => {
      const existingProject = createMockProject();
      mockQueryOne.mockResolvedValue(existingProject);

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

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

  describe('delete', () => {
    it('should soft delete project', async () => {
      const project = createMockProject();
      mockQueryOne.mockResolvedValue(project);
      mockQuery.mockResolvedValue([]);

      await projectsService.delete('project-uuid-1', tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('deleted_at = CURRENT_TIMESTAMP'),
        expect.any(Array)
      );
    });

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

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

  describe('getStats', () => {
    it('should return project statistics', async () => {
      const project = createMockProject();
      const stats = {
        total_tasks: 10,
        completed_tasks: 5,
        in_progress_tasks: 3,
        total_hours: 40,
        total_milestones: 3,
        completed_milestones: 1,
      };

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

      const result = await projectsService.getStats('project-uuid-1', tenantId);

      expect(result).toMatchObject({
        total_tasks: 10,
        completed_tasks: 5,
        completion_percentage: 50,
      });
    });

    it('should return 0% completion when no tasks', async () => {
      const project = createMockProject();
      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 result = await projectsService.getStats('project-uuid-1', tenantId);

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