// src/services/emailService.server.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';


// Use vi.hoisted to define mocks that need to be available inside vi.mock factories.
// This prevents "Cannot access before initialization" errors.
const mocks = vi.hoisted(() => ({
  sendMail: vi.fn().mockImplementation((mailOptions) => {
    console.log('[TEST DEBUG] mockSendMail called with:', mailOptions?.to);
    return Promise.resolve({
      messageId: 'default-id',
      message: Buffer.from('mock-email-content'),
    });
  }),
}));

vi.mock('nodemailer', () => {
  return {
    // Mock the default export which is what the service uses
    default: {
      // The createTransport function returns an object with a sendMail method
      createTransport: vi.fn(() => ({ sendMail: mocks.sendMail })),
    },
  };
});

// Mock the logger to prevent console output during tests
vi.mock('./logger.server', () => ({
  logger: {
    info: vi.fn(),
    debug: vi.fn(),
    error: vi.fn(),
  },
}));

// Now that mocks are set up, we can import the service under test.
import { sendPasswordResetEmail, sendWelcomeEmail, sendDealNotificationEmail } from './emailService.server';
import type { WatchedItemDeal } from '../types';
import { logger } from './logger.server';

describe('Email Service (Server)', () => {
  beforeEach(async () => {
    console.log('[TEST SETUP] Setting up Email Service mocks');
    vi.clearAllMocks();
    // Reset to default successful implementation
    mocks.sendMail.mockImplementation((mailOptions: { to: string; }) => {
      console.log('[TEST DEBUG] mockSendMail (default) called with:', mailOptions?.to);
      return Promise.resolve({
        messageId: 'default-mock-id',
        message: Buffer.from('mock-email-content'),
      });
    });
  });

  describe('sendPasswordResetEmail', () => {
    it('should call sendMail with the correct recipient, subject, and link', async () => {
      const to = 'test@example.com';
      const resetLink = 'http://localhost:3000/reset/mock-token-123';

      await sendPasswordResetEmail(to, resetLink, logger);

      expect(mocks.sendMail).toHaveBeenCalledTimes(1);
      const mailOptions = mocks.sendMail.mock.calls[0][0] as { to: string, subject: string, text: string, html: string };

      expect(mailOptions.to).toBe(to);
      expect(mailOptions.subject).toBe('Your Password Reset Request');
      expect(mailOptions.text).toContain(resetLink);
      // The implementation constructs the link, so we check that our mock link is present inside the href
      expect(mailOptions.html).toContain(resetLink);
    });
  });

  describe('sendWelcomeEmail', () => {
    it('should send a personalized welcome email when a name is provided', async () => {
      const to = 'new.user@example.com';
      const name = 'Jane Doe';

      mocks.sendMail.mockResolvedValue({ messageId: 'test-id', message: Buffer.from('content') });

      await sendWelcomeEmail(to, name, logger);

      expect(mocks.sendMail).toHaveBeenCalledTimes(1);
      const mailOptions = mocks.sendMail.mock.calls[0][0] as { to: string, subject: string, text: string, html: string };

      expect(mailOptions.to).toBe(to);
      expect(mailOptions.subject).toBe('Welcome to Flyer Crawler!');
      expect(mailOptions.text).toContain('Hello Jane Doe,');
      // Check for the key content instead of the exact HTML structure
      expect(mailOptions.html).toContain('Hello Jane Doe,');
    });

    it('should send a generic welcome email when a name is not provided', async () => {
      const to = 'another.user@example.com';

      mocks.sendMail.mockResolvedValue({ messageId: 'test-id', message: Buffer.from('content') });

      await sendWelcomeEmail(to, null, logger);

      expect(mocks.sendMail).toHaveBeenCalledTimes(1);
      const mailOptions = mocks.sendMail.mock.calls[0][0] as { to: string, subject: string, text: string, html: string };

      expect(mailOptions.text).toContain('Hello there,');
      // Check for the key content instead of the exact HTML structure
      expect(mailOptions.html).toContain('Hello there,');
    });
  });

  describe('sendDealNotificationEmail', () => {
    const mockDeals = [
      { item_name: 'Apples', best_price_in_cents: 199, store_name: 'Green Grocer' },
      { item_name: 'Milk', best_price_in_cents: 350, store_name: 'Dairy Farm' },
    ];

    it('should send a personalized email with a list of deals', async () => {
      const to = 'deal.hunter@example.com';
      const name = 'Deal Hunter';

      await sendDealNotificationEmail(to, name, mockDeals as Partial<WatchedItemDeal>[] as WatchedItemDeal[], logger);

      expect(mocks.sendMail).toHaveBeenCalledTimes(1);
      const mailOptions = mocks.sendMail.mock.calls[0][0] as { to: string, subject: string, text: string, html: string };

      expect(mailOptions.to).toBe(to);
      expect(mailOptions.subject).toBe('New Deals Found on Your Watched Items!');
      // FIX: Use `stringContaining` to check for key parts of the HTML without being brittle about whitespace.
      // The actual HTML is a multi-line template string with tags like <h1>, <ul>, and <li>.
      expect(mailOptions.html).toEqual(expect.stringContaining('<h1>Hi Deal Hunter,</h1>'));
      expect(mailOptions.html).toEqual(expect.stringContaining(
        '<li>\n          <strong>Apples</strong> is on sale for \n          <strong>$1.99</strong> \n          at Green Grocer!\n        </li>'
      ));
      expect(mailOptions.html).toEqual(expect.stringContaining(
        '<li>\n          <strong>Milk</strong> is on sale for \n          <strong>$3.50</strong> \n          at Dairy Farm!\n        </li>'
      ));
      expect(mailOptions.html).toEqual(expect.stringContaining('<p>Check them out on the deals page!</p>'));
    });

    it('should send a generic email when name is null', async () => {
      const to = 'anonymous.user@example.com';

      await sendDealNotificationEmail(to, null, mockDeals as Partial<WatchedItemDeal>[] as WatchedItemDeal[], logger);

      expect(mocks.sendMail).toHaveBeenCalledTimes(1);
      const mailOptions = mocks.sendMail.mock.calls[0][0] as { to: string, subject: string, html: string };

      expect(mailOptions.to).toBe(to);
      expect(mailOptions.html).toContain('Hi there,');
    });

    it('should log an error if sendMail fails', async () => {
      const to = 'fail@example.com';
      const name = 'Failure';
      const emailError = new Error('SMTP Connection Failed');
      mocks.sendMail.mockRejectedValue(emailError);

      await expect(sendDealNotificationEmail(to, name, mockDeals as Partial<WatchedItemDeal>[] as WatchedItemDeal[], logger)).rejects.toThrow(emailError);

      expect(logger.error).toHaveBeenCalledWith(
        { err: emailError, to, subject: 'New Deals Found on Your Watched Items!' },
        'Failed to send email.'
      );
    });
  });
});