/**
 * Activities Service Unit Tests
 *
 * Tests for CRM activities (calls, meetings, emails, tasks, notes)
 */

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

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

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

// Mock logger
jest.mock('../../../shared/utils/logger.js', () => ({
  logger: {
    info: jest.fn(),
    error: jest.fn(),
    warn: jest.fn(),
    debug: jest.fn(),
  },
}));

// Import after mocking
import { activitiesService } from '../activities.service.js';
import { NotFoundError, ValidationError } from '../../../shared/errors/index.js';

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

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

  describe('findAll', () => {
    it('should return activities with pagination', async () => {
      const mockActivities = [
        createMockActivity({ id: '1', name: 'Call 1' }),
        createMockActivity({ id: '2', name: 'Meeting 1' }),
      ];

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

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

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

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

      await activitiesService.findAll(tenantId, { activity_type: 'call' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.activity_type = $'),
        expect.arrayContaining([tenantId, 'call'])
      );
    });

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

      await activitiesService.findAll(tenantId, { status: 'scheduled' });

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

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

      await activitiesService.findAll(tenantId, { res_model: 'opportunity', res_id: 'opp-uuid-1' });

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.res_model = $'),
        expect.arrayContaining([tenantId, 'opportunity', 'opp-uuid-1'])
      );
    });

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

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

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.scheduled_date >= $'),
        expect.any(Array)
      );
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.scheduled_date <= $'),
        expect.any(Array)
      );
    });

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

      await activitiesService.findAll(tenantId, { search: 'demo' });

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

  describe('findById', () => {
    it('should return activity when found', async () => {
      const mockActivity = createMockActivity();
      mockQueryOne.mockResolvedValue(mockActivity);

      const result = await activitiesService.findById('activity-uuid-1', tenantId);

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

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

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

    it('should fetch resource name for linked activity', async () => {
      const mockActivity = createMockActivity({
        res_model: 'opportunity',
        res_id: 'opp-uuid-1',
      });

      mockQueryOne
        .mockResolvedValueOnce(mockActivity) // findById
        .mockResolvedValueOnce({ name: 'Big Deal Opportunity' }); // getResourceName

      const result = await activitiesService.findById('activity-uuid-1', tenantId);

      expect(result.res_name).toBe('Big Deal Opportunity');
    });
  });

  describe('create', () => {
    const createDto = {
      company_id: companyId,
      activity_type: 'call' as const,
      name: 'Initial call with prospect',
      res_model: 'lead',
      res_id: 'lead-uuid-1',
      user_id: userId,
    };

    it('should create activity successfully', async () => {
      const createdActivity = createMockActivity({ ...createDto });
      mockQueryOne
        .mockResolvedValueOnce(createdActivity) // INSERT
        .mockResolvedValueOnce(createdActivity); // findById
      mockQuery.mockResolvedValueOnce([]); // Update last activity date

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

      expect(result.name).toBe(createDto.name);
      expect(result.activity_type).toBe('call');
    });

    it('should create activity with scheduled date', async () => {
      const scheduledDate = new Date().toISOString();
      const dto = { ...createDto, scheduled_date: scheduledDate };
      const createdActivity = createMockActivity({ ...dto });

      mockQueryOne
        .mockResolvedValueOnce(createdActivity)
        .mockResolvedValueOnce(createdActivity);
      mockQuery.mockResolvedValueOnce([]);

      await activitiesService.create(dto, tenantId, userId);

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO crm.activities'),
        expect.arrayContaining([scheduledDate])
      );
    });

    it('should update last activity date on linked resource', async () => {
      const dto = {
        ...createDto,
        res_model: 'opportunity',
        res_id: 'opp-uuid-1',
      };
      const createdActivity = createMockActivity(dto);

      mockQueryOne
        .mockResolvedValueOnce(createdActivity)
        .mockResolvedValueOnce(createdActivity);
      mockQuery.mockResolvedValueOnce([]);

      await activitiesService.create(dto, tenantId, userId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('UPDATE crm.opportunities SET date_last_activity'),
        expect.arrayContaining(['opp-uuid-1', tenantId])
      );
    });
  });

  describe('update', () => {
    it('should update activity successfully', async () => {
      const existingActivity = createMockActivity({ status: 'scheduled' });
      mockQueryOne.mockResolvedValue(existingActivity);
      mockQuery.mockResolvedValue([]);

      await activitiesService.update(
        'activity-uuid-1',
        { name: 'Updated Activity Name' },
        tenantId,
        userId
      );

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

    it('should throw ValidationError when activity is done', async () => {
      const doneActivity = createMockActivity({ status: 'done' });
      mockQueryOne.mockResolvedValue(doneActivity);

      await expect(
        activitiesService.update('activity-uuid-1', { name: 'New name' }, tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should return existing activity when no fields to update', async () => {
      const existingActivity = createMockActivity({ status: 'scheduled' });
      mockQueryOne.mockResolvedValue(existingActivity);

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

      expect(result).toEqual(existingActivity);
      expect(mockQuery).not.toHaveBeenCalled();
    });
  });

  describe('markDone', () => {
    it('should mark activity as done', async () => {
      const scheduledActivity = createMockActivity({ status: 'scheduled', res_model: null, res_id: null });
      const doneActivity = createMockActivity({ ...scheduledActivity, status: 'done' });

      mockQueryOne
        .mockResolvedValueOnce(scheduledActivity) // findById before update
        .mockResolvedValueOnce(doneActivity); // findById after update
      mockQuery.mockResolvedValueOnce([]); // UPDATE activity

      await activitiesService.markDone('activity-uuid-1', tenantId, userId);

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

    it('should add notes when marking as done', async () => {
      const scheduledActivity = createMockActivity({ status: 'scheduled' });
      mockQueryOne.mockResolvedValue(scheduledActivity);
      mockQuery.mockResolvedValueOnce([]);
      mockQuery.mockResolvedValueOnce([]);

      await activitiesService.markDone(
        'activity-uuid-1',
        tenantId,
        userId,
        'Great conversation about the product'
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('notes = COALESCE($1, notes)'),
        expect.arrayContaining(['Great conversation about the product'])
      );
    });

    it('should throw ValidationError when activity is already done', async () => {
      const doneActivity = createMockActivity({ status: 'done' });
      mockQueryOne.mockResolvedValue(doneActivity);

      await expect(
        activitiesService.markDone('activity-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when activity is cancelled', async () => {
      const cancelledActivity = createMockActivity({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledActivity);

      await expect(
        activitiesService.markDone('activity-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('cancel', () => {
    it('should cancel scheduled activity', async () => {
      const scheduledActivity = createMockActivity({ status: 'scheduled', res_model: null, res_id: null });
      const cancelledActivity = createMockActivity({ ...scheduledActivity, status: 'cancelled' });

      mockQueryOne
        .mockResolvedValueOnce(scheduledActivity) // findById before cancel
        .mockResolvedValueOnce(cancelledActivity); // findById after cancel
      mockQuery.mockResolvedValueOnce([]);

      await activitiesService.cancel('activity-uuid-1', tenantId, userId);

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

    it('should throw ValidationError when activity is done', async () => {
      const doneActivity = createMockActivity({ status: 'done' });
      mockQueryOne.mockResolvedValue(doneActivity);

      await expect(
        activitiesService.cancel('activity-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when activity is already cancelled', async () => {
      const cancelledActivity = createMockActivity({ status: 'cancelled' });
      mockQueryOne.mockResolvedValue(cancelledActivity);

      await expect(
        activitiesService.cancel('activity-uuid-1', tenantId, userId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('delete', () => {
    it('should delete scheduled activity', async () => {
      const scheduledActivity = createMockActivity({ status: 'scheduled' });
      mockQueryOne.mockResolvedValue(scheduledActivity);
      mockQuery.mockResolvedValue([]);

      await activitiesService.delete('activity-uuid-1', tenantId);

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('DELETE FROM crm.activities'),
        expect.any(Array)
      );
    });

    it('should throw ValidationError when trying to delete done activity', async () => {
      const doneActivity = createMockActivity({ status: 'done' });
      mockQueryOne.mockResolvedValue(doneActivity);

      await expect(
        activitiesService.delete('activity-uuid-1', tenantId)
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('getResourceActivities', () => {
    it('should return activities for opportunity', async () => {
      const activities = [
        createMockActivity({ id: '1', name: 'Call 1', activity_type: 'call' }),
        createMockActivity({ id: '2', name: 'Meeting 1', activity_type: 'meeting' }),
      ];
      mockQuery.mockResolvedValueOnce(activities);

      const result = await activitiesService.getResourceActivities(
        'opportunity',
        'opp-uuid-1',
        tenantId
      );

      expect(result).toHaveLength(2);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.res_model = $1 AND a.res_id = $2'),
        expect.arrayContaining(['opportunity', 'opp-uuid-1', tenantId])
      );
    });

    it('should filter by status when provided', async () => {
      mockQuery.mockResolvedValue([]);

      await activitiesService.getResourceActivities(
        'lead',
        'lead-uuid-1',
        tenantId,
        'scheduled'
      );

      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('a.status = $4'),
        expect.arrayContaining(['lead', 'lead-uuid-1', tenantId, 'scheduled'])
      );
    });
  });

  describe('getActivitySummary', () => {
    it('should return activity summary', async () => {
      mockQueryOne.mockResolvedValue({
        total: '15',
        scheduled: '8',
        done: '5',
        cancelled: '2',
        overdue: '3',
      });

      mockQuery.mockResolvedValue([
        { activity_type: 'call', count: '6' },
        { activity_type: 'meeting', count: '4' },
        { activity_type: 'email', count: '3' },
        { activity_type: 'task', count: '2' },
      ]);

      const result = await activitiesService.getActivitySummary(tenantId);

      expect(result.total_activities).toBe(15);
      expect(result.scheduled).toBe(8);
      expect(result.done).toBe(5);
      expect(result.overdue).toBe(3);
      expect(result.by_type.call).toBe(6);
      expect(result.by_type.meeting).toBe(4);
    });

    it('should filter by user when provided', async () => {
      mockQueryOne.mockResolvedValue({
        total: '5',
        scheduled: '3',
        done: '2',
        cancelled: '0',
        overdue: '1',
      });
      mockQuery.mockResolvedValue([]);

      await activitiesService.getActivitySummary(tenantId, userId);

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

    it('should filter by date range', async () => {
      mockQueryOne.mockResolvedValue({
        total: '10',
        scheduled: '5',
        done: '5',
        cancelled: '0',
        overdue: '0',
      });
      mockQuery.mockResolvedValue([]);

      await activitiesService.getActivitySummary(
        tenantId,
        undefined,
        '2026-01-01',
        '2026-01-31'
      );

      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('scheduled_date >= $'),
        expect.any(Array)
      );
    });
  });

  describe('scheduleFollowUp', () => {
    it('should schedule follow-up inheriting resource info', async () => {
      // No res_model on completed activity to avoid getResourceName call
      const completedActivity = createMockActivity({
        id: 'completed-uuid',
        status: 'done',
        res_model: null,
        res_id: null,
        partner_id: 'partner-uuid-1',
      });

      const followUpDto = {
        company_id: companyId,
        activity_type: 'meeting' as const,
        name: 'Follow-up meeting',
        scheduled_date: new Date().toISOString(),
        res_model: 'opportunity',
        res_id: 'opp-uuid-1',
      };

      const newActivity = createMockActivity({
        activity_type: 'meeting',
        name: 'Follow-up meeting',
        res_model: 'opportunity',
        res_id: 'opp-uuid-1',
        partner_id: 'partner-uuid-1',
      });

      // Mock sequence:
      // 1. findById(completedActivity) - no res_model, so no getResourceName
      // 2. create:
      //    - INSERT returns newActivity
      //    - findById(newActivity) - has res_model, so calls getResourceName
      mockQueryOne
        .mockResolvedValueOnce(completedActivity) // findById for completed (no getResourceName since res_model is null)
        .mockResolvedValueOnce(newActivity) // INSERT new activity
        .mockResolvedValueOnce(newActivity) // findById for new activity
        .mockResolvedValueOnce({ name: 'Test Opportunity' }); // getResourceName for new activity
      mockQuery.mockResolvedValueOnce([]); // Update last activity date

      const result = await activitiesService.scheduleFollowUp(
        'completed-uuid',
        followUpDto,
        tenantId,
        userId
      );

      expect(result.res_model).toBe('opportunity');
      expect(result.res_id).toBe('opp-uuid-1');
      expect(result.partner_id).toBe('partner-uuid-1');
    });

    it('should allow overriding resource info in follow-up', async () => {
      // No res_model to simplify mock sequence
      const completedActivity = createMockActivity({
        id: 'completed-uuid',
        res_model: null,
        res_id: null,
      });

      const followUpDto = {
        company_id: companyId,
        activity_type: 'call' as const,
        name: 'New opportunity call',
        res_model: 'opportunity',
        res_id: 'new-opp-uuid',
      };

      const newActivity = createMockActivity({
        activity_type: 'call',
        name: 'New opportunity call',
        res_model: 'opportunity',
        res_id: 'new-opp-uuid',
      });

      mockQueryOne
        .mockResolvedValueOnce(completedActivity) // findById completed (no getResourceName)
        .mockResolvedValueOnce(newActivity) // INSERT
        .mockResolvedValueOnce(newActivity) // findById new
        .mockResolvedValueOnce({ name: 'New Opportunity' }); // getResourceName
      mockQuery.mockResolvedValueOnce([]); // Update last activity

      const result = await activitiesService.scheduleFollowUp(
        'completed-uuid',
        followUpDto,
        tenantId,
        userId
      );

      expect(result.res_model).toBe('opportunity');
      expect(result.res_id).toBe('new-opp-uuid');
    });
  });

  describe('getOverdueCount', () => {
    it('should return count of overdue activities', async () => {
      mockQueryOne.mockResolvedValueOnce({ count: '7' });

      const result = await activitiesService.getOverdueCount(tenantId);

      expect(result).toBe(7);
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining("status = 'scheduled'"),
        expect.arrayContaining([tenantId])
      );
      expect(mockQueryOne).toHaveBeenCalledWith(
        expect.stringContaining('scheduled_date < CURRENT_DATE'),
        expect.any(Array)
      );
    });

    it('should filter by user when provided', async () => {
      mockQueryOne.mockResolvedValueOnce({ count: '3' });

      await activitiesService.getOverdueCount(tenantId, userId);

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

  describe('Activity Types', () => {
    it.each([
      ['call', 'Initial sales call'],
      ['meeting', 'Product demo'],
      ['email', 'Follow-up email'],
      ['task', 'Prepare proposal'],
      ['note', 'Customer feedback'],
      ['other', 'Site visit'],
    ])('should create %s activity', async (activityType, name) => {
      // Clear mocks before each iteration
      jest.clearAllMocks();

      const dto = {
        company_id: companyId,
        activity_type: activityType as any,
        name,
        res_model: null,
        res_id: null,
      };

      const createdActivity = createMockActivity({
        ...dto,
        activity_type: activityType as any,
        name,
      });

      mockQueryOne
        .mockResolvedValueOnce(createdActivity) // INSERT
        .mockResolvedValueOnce(createdActivity); // findById (no getResourceName since res_model is null)

      const result = await activitiesService.create(dto, tenantId, userId);

      expect(result.activity_type).toBe(activityType);
      expect(result.name).toBe(name);
    });
  });
});
