import { Test, TestingModule } from '@nestjs/testing';
import { EmailService } from './email.service';
import { PrismaService } from '../../prisma/prisma.service';
import { getQueueToken } from '@nestjs/bullmq';

// Use string literals since Prisma client may not be regenerated yet
const EmailType = {
  WELCOME: 'WELCOME' as const,
  TRAINING_REMINDER: 'TRAINING_REMINDER' as const,
  MEMBERSHIP_EXPIRING: 'MEMBERSHIP_EXPIRING' as const,
  PROMO: 'PROMO' as const,
  BIRTHDAY: 'BIRTHDAY' as const,
};

const mockPrisma = {
  emailTemplate: {
    findMany: jest.fn(),
    findUnique: jest.fn(),
    upsert: jest.fn(),
    delete: jest.fn(),
  },
  emailLog: {
    create: jest.fn(),
    findFirst: jest.fn(),
    findMany: jest.fn(),
    update: jest.fn(),
    count: jest.fn(),
  },
};

const mockQueue = {
  add: jest.fn().mockResolvedValue({}),
};

describe('EmailService', () => {
  let service: EmailService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        EmailService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: getQueueToken('email-send'), useValue: mockQueue },
      ],
    }).compile();

    service = module.get<EmailService>(EmailService);
    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('findTemplates', () => {
    it('should return templates for a club', async () => {
      const templates = [
        { id: '1', clubId: 'club-1', type: 'WELCOME', subject: 'Test' },
      ];
      mockPrisma.emailTemplate.findMany.mockResolvedValue(templates);

      const result = await service.findTemplates('club-1');
      expect(result).toEqual(templates);
      expect(mockPrisma.emailTemplate.findMany).toHaveBeenCalledWith({
        where: { clubId: 'club-1' },
        orderBy: { type: 'asc' },
      });
    });
  });

  describe('upsertTemplate', () => {
    it('should create or update a template', async () => {
      const dto = {
        type: EmailType.WELCOME,
        subject: 'Welcome {{clientName}}',
        bodyHtml: '<h1>Welcome</h1>',
      };
      const expected = { id: '1', clubId: 'club-1', ...dto };
      mockPrisma.emailTemplate.upsert.mockResolvedValue(expected);

      const result = await service.upsertTemplate('club-1', dto);
      expect(result).toEqual(expected);
      expect(mockPrisma.emailTemplate.upsert).toHaveBeenCalledWith({
        where: { clubId_type: { clubId: 'club-1', type: 'WELCOME' } },
        create: expect.objectContaining({
          clubId: 'club-1',
          type: 'WELCOME',
          subject: dto.subject,
          bodyHtml: dto.bodyHtml,
        }),
        update: expect.objectContaining({
          subject: dto.subject,
          bodyHtml: dto.bodyHtml,
        }),
      });
    });
  });

  describe('send', () => {
    it('should create email log and queue job', async () => {
      const clubId = 'club-1';
      const dto = {
        recipientEmail: 'test@example.com',
        type: EmailType.WELCOME,
        variables: { clientName: 'John', clubName: 'FitClub' },
      };

      mockPrisma.emailTemplate.findUnique.mockResolvedValue(null);
      mockPrisma.emailLog.create.mockResolvedValue({
        id: 'log-1',
        clubId,
        ...dto,
        status: 'PENDING',
      });

      const result = await service.send(clubId, dto);

      expect(result).toEqual({ emailLogId: 'log-1', status: 'queued' });
      expect(mockPrisma.emailLog.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clubId,
          recipientEmail: dto.recipientEmail,
          type: 'WELCOME',
          status: 'PENDING',
        }),
      });
      expect(mockQueue.add).toHaveBeenCalledWith(
        'send',
        expect.objectContaining({
          emailLogId: 'log-1',
          to: dto.recipientEmail,
        }),
        expect.objectContaining({ attempts: 3 }),
      );
    });

    it('should use template subject and body when available', async () => {
      const clubId = 'club-1';
      const dto = {
        recipientEmail: 'test@example.com',
        type: EmailType.WELCOME,
        variables: { clientName: 'Anna' },
      };

      mockPrisma.emailTemplate.findUnique.mockResolvedValue({
        id: 'tmpl-1',
        subject: 'Hello {{clientName}}!',
        bodyHtml: '<h1>Hello {{clientName}}</h1>',
        bodyText: 'Hello {{clientName}}',
      });
      mockPrisma.emailLog.create.mockResolvedValue({ id: 'log-2' });

      await service.send(clubId, dto);

      expect(mockQueue.add).toHaveBeenCalledWith(
        'send',
        expect.objectContaining({
          subject: 'Hello Anna!',
          html: '<h1>Hello Anna</h1>',
          text: 'Hello Anna',
        }),
        expect.any(Object),
      );
    });
  });

  describe('findLogs', () => {
    it('should return paginated logs', async () => {
      const logs = [
        { id: '1', status: 'SENT' },
        { id: '2', status: 'DELIVERED' },
      ];
      mockPrisma.emailLog.findMany.mockResolvedValue(logs);

      const result = await service.findLogs('club-1', { limit: 20 });

      expect(result.data).toEqual(logs);
      expect(result.hasMore).toBe(false);
    });

    it('should handle pagination with cursor', async () => {
      const logs = Array.from({ length: 21 }, (_, i) => ({
        id: `log-${i}`,
        status: 'SENT',
      }));
      mockPrisma.emailLog.findMany.mockResolvedValue(logs);

      const result = await service.findLogs('club-1', { limit: 20 });

      expect(result.data).toHaveLength(20);
      expect(result.hasMore).toBe(true);
      expect(result.nextCursor).toBe('log-19');
    });
  });

  describe('getStats', () => {
    it('should return email statistics', async () => {
      mockPrisma.emailLog.count
        .mockResolvedValueOnce(100) // total
        .mockResolvedValueOnce(40) // sent
        .mockResolvedValueOnce(35) // delivered
        .mockResolvedValueOnce(20) // opened
        .mockResolvedValueOnce(5); // failed

      const result = await service.getStats('club-1');

      expect(result).toEqual({
        total: 100,
        sent: 40,
        delivered: 35,
        opened: 20,
        failed: 5,
        deliveryRate: '55.0',
        openRate: '57.1',
      });
    });
  });

  describe('updateStatus', () => {
    it('should update email log status via provider message ID', async () => {
      mockPrisma.emailLog.findFirst.mockResolvedValue({
        id: 'log-1',
        providerMessageId: 'msg-123',
      });

      await service.updateStatus('msg-123', 'DELIVERED');

      expect(mockPrisma.emailLog.update).toHaveBeenCalledWith({
        where: { id: 'log-1' },
        data: expect.objectContaining({
          status: 'DELIVERED',
          deliveredAt: expect.any(Date),
        }),
      });
    });

    it('should do nothing if message ID not found', async () => {
      mockPrisma.emailLog.findFirst.mockResolvedValue(null);

      await service.updateStatus('unknown', 'DELIVERED');

      expect(mockPrisma.emailLog.update).not.toHaveBeenCalled();
    });
  });
});
