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

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

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

  describe('findAll', () => {
    it('should return tasks with pagination', async () => {
      const mockTasks = [
        createMockTask({ id: '1', name: 'Task 1' }),
        createMockTask({ id: '2', name: 'Task 2' }),
      ];

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

      const result = await tasksService.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 tasksService.findAll(tenantId, { project_id: 'project-uuid' });

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

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

      await tasksService.findAll(tenantId, { status: 'in_progress' });

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

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

      await tasksService.findAll(tenantId, { assigned_to: 'user-uuid' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('t.assigned_to = $'),
        expect.arrayContaining([tenantId, 'user-uuid'])
      );
    });

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

      await tasksService.findAll(tenantId, { priority: 'high' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('t.priority = $'),
        expect.arrayContaining([tenantId, 'high'])
      );
    });

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

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

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

  describe('findById', () => {
    it('should return task when found', async () => {
      const mockTask = createMockTask();
      mockQueryOne.mockResolvedValue(mockTask);

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

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

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

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

  describe('create', () => {
    const createDto = {
      project_id: 'project-uuid',
      name: 'New Task',
    };

    it('should create task with auto-generated sequence', async () => {
      const createdTask = createMockTask({ ...createDto, sequence: 1 });
      mockQueryOne
        .mockResolvedValueOnce({ max_seq: 1 }) // sequence
        .mockResolvedValueOnce(createdTask); // INSERT

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

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

    it('should create task with default priority', async () => {
      const createdTask = createMockTask({ ...createDto, priority: 'normal' });
      mockQueryOne
        .mockResolvedValueOnce({ max_seq: 1 })
        .mockResolvedValueOnce(createdTask);

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

      expect(result.priority).toBe('normal');
    });
  });

  describe('update', () => {
    it('should update task successfully', async () => {
      const existingTask = createMockTask();
      mockQueryOne.mockResolvedValue(existingTask);
      mockQuery.mockResolvedValue([]);

      await tasksService.update(
        'task-uuid-1',
        { name: 'Updated Task' },
        tenantId,
        userId
      );

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

    it('should throw ValidationError when setting task as its own parent', async () => {
      const existingTask = createMockTask();
      mockQueryOne.mockResolvedValue(existingTask);

      await expect(
        tasksService.update('task-uuid-1', { parent_id: 'task-uuid-1' }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should return unchanged task when no fields to update', async () => {
      const existingTask = createMockTask();
      mockQueryOne.mockResolvedValue(existingTask);

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

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

    it('should update task status', async () => {
      const existingTask = createMockTask({ status: 'todo' });
      mockQueryOne.mockResolvedValue(existingTask);
      mockQuery.mockResolvedValue([]);

      await tasksService.update('task-uuid-1', { status: 'done' }, tenantId, userId);

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

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

      await tasksService.delete('task-uuid-1', tenantId, userId);

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

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

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

  describe('move', () => {
    it('should move task to new stage and position', async () => {
      const task = createMockTask();
      mockQueryOne.mockResolvedValue(task);
      mockQuery.mockResolvedValue([]);

      await tasksService.move('task-uuid-1', 'new-stage-uuid', 5, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('stage_id = $1, sequence = $2'),
        expect.arrayContaining(['new-stage-uuid', 5, userId])
      );
    });

    it('should move task to no stage (null)', async () => {
      const task = createMockTask();
      mockQueryOne.mockResolvedValue(task);
      mockQuery.mockResolvedValue([]);

      await tasksService.move('task-uuid-1', null, 1, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('stage_id = $1'),
        expect.arrayContaining([null, 1, userId])
      );
    });
  });

  describe('assign', () => {
    it('should assign task to user', async () => {
      const task = createMockTask();
      mockQueryOne.mockResolvedValue(task);
      mockQuery.mockResolvedValue([]);

      await tasksService.assign('task-uuid-1', 'new-user-uuid', tenantId, userId);

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

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

      await expect(
        tasksService.assign('nonexistent-id', 'user-uuid', tenantId, userId)
      ).rejects.toThrow(NotFoundError);
    });
  });
});
