import { describe, it, expect, vi, beforeEach } from 'vitest';
import nodemailer from 'nodemailer';

const mockQuery = vi.fn();
vi.mock('../db.js', () => ({
  getPool: () => ({ query: mockQuery }),
}));
vi.mock('nodemailer', () => ({
  default: { createTransport: vi.fn() },
}));

import {
  createInvoice,
  generateMonthlyInvoice,
  getInvoiceByNumber,
  emailInvoice,
  type Invoice,
} from './invoices.js';

const baseInvoice: Invoice = {
  id: 1,
  customer_id: 'cust-1',
  invoice_number: 'SMCP-20260501-aabb1122',
  amount: 25.0,
  currency: 'USD',
  status: 'draft',
  period_start: '2026-04-01',
  period_end: '2026-04-30',
  line_items: [{ description: 'email actions', quantity: 500, unit_price: 0.05, amount: 25.0 }],
  sent_at: null,
  paid_at: null,
  created_at: '2026-05-01T00:00:00Z',
  constructor: { name: 'RowDataPacket' } as any,
};

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

// ── generateInvoiceNumber format ─────────────────────────────────────────────

describe('generateMonthlyInvoice — billing period', () => {
  it('queries previous month, not current', async () => {
    mockQuery.mockResolvedValueOnce([[]]); // usage query returns empty
    await generateMonthlyInvoice('cust-1');
    const sql: string = mockQuery.mock.calls[0][0];
    // Must reference DATE_SUB for the start of the previous month
    expect(sql).toContain('DATE_SUB');
    // Must NOT use a plain NOW() as the lower bound (that would be current month)
    expect(sql).not.toMatch(/>=\s*DATE_FORMAT\(NOW\(\)/);
  });

  it('returns null when customer has zero usage', async () => {
    mockQuery.mockResolvedValueOnce([[]]); // no usage rows
    const result = await generateMonthlyInvoice('cust-1');
    expect(result).toBeNull();
  });

  it('creates invoice for previous month period', async () => {
    const now = new Date();
    const expectedStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
      .toISOString().slice(0, 10);

    mockQuery
      .mockResolvedValueOnce([[{ platform: 'email', count: 100 }]]) // usage
      .mockResolvedValueOnce([[]]) // insert
      .mockResolvedValueOnce([[{ ...baseInvoice, period_start: expectedStart }]]); // select after insert

    const invoice = await generateMonthlyInvoice('cust-1');
    expect(invoice?.period_start).toBe(expectedStart);
  });
});

// ── Invoice number format ─────────────────────────────────────────────────────

describe('createInvoice — invoice number', () => {
  it('generates hex suffix, not decimal padded', async () => {
    mockQuery
      .mockResolvedValueOnce([[]]) // insert
      .mockResolvedValueOnce([[baseInvoice]]); // select

    await createInvoice('cust-1', 25, baseInvoice.line_items, '2026-04-01', '2026-04-30');

    const insertSql: string = mockQuery.mock.calls[0][0];
    const invoiceNumber: string = mockQuery.mock.calls[0][1][1];
    // Should be SMCP-YYYYMMDD-<8 hex chars>
    expect(invoiceNumber).toMatch(/^SMCP-\d{8}-[0-9a-f]{8}$/);
  });
});

// ── Idempotency ───────────────────────────────────────────────────────────────

describe('createInvoice — idempotency', () => {
  it('returns existing invoice on uq_customer_period duplicate', async () => {
    const dupError = Object.assign(new Error("Duplicate entry 'cust-1-2026-04-01' for key 'invoices.uq_customer_period'"), { errno: 1062 });
    mockQuery
      .mockRejectedValueOnce(dupError) // INSERT throws 1062
      .mockResolvedValueOnce([[baseInvoice]]); // SELECT existing

    const invoice = await createInvoice('cust-1', 25, baseInvoice.line_items, '2026-04-01', '2026-04-30');
    expect(invoice.invoice_number).toBe(baseInvoice.invoice_number);
    expect(invoice.customer_id).toBe('cust-1');
  });

  it('re-throws 1062 for other constraints (e.g. duplicate invoice_number)', async () => {
    const dupError = Object.assign(new Error("Duplicate entry 'SMCP-...' for key 'invoices.invoice_number'"), { errno: 1062 });
    mockQuery.mockRejectedValueOnce(dupError);

    await expect(createInvoice('cust-1', 25, baseInvoice.line_items, '2026-04-01', '2026-04-30'))
      .rejects.toThrow('invoice_number');
  });
});

// ── line_items JSON normalization ─────────────────────────────────────────────

describe('getInvoiceByNumber — line_items normalization', () => {
  it('parses line_items when returned as JSON string', async () => {
    const rawInvoice = {
      ...baseInvoice,
      line_items: JSON.stringify(baseInvoice.line_items) as any,
    };
    mockQuery.mockResolvedValueOnce([[rawInvoice]]);

    const invoice = await getInvoiceByNumber('SMCP-20260501-aabb1122');
    expect(Array.isArray(invoice?.line_items)).toBe(true);
    expect(invoice?.line_items[0].description).toBe('email actions');
  });

  it('leaves line_items unchanged when already an array', async () => {
    mockQuery.mockResolvedValueOnce([[baseInvoice]]);
    const invoice = await getInvoiceByNumber('SMCP-20260501-aabb1122');
    expect(Array.isArray(invoice?.line_items)).toBe(true);
  });
});

// ── emailInvoice ─────────────────────────────────────────────────────────────

describe('emailInvoice', () => {
  it('sends mail with invoice_number in subject and HTML body', async () => {
    const sendMail = vi.fn().mockResolvedValue({ messageId: '<abc@mail>' });
    vi.mocked(nodemailer.createTransport).mockReturnValue({ sendMail } as any);

    await emailInvoice(baseInvoice, 'customer@example.com');

    expect(sendMail).toHaveBeenCalledOnce();
    const mailArgs = sendMail.mock.calls[0][0];
    expect(mailArgs.to).toBe('customer@example.com');
    expect(mailArgs.subject).toContain(baseInvoice.invoice_number);
    expect(mailArgs.html).toContain(baseInvoice.invoice_number);
  });
});
