import {
  exportToCSV,
  exportToExcel,
  exportToJSON,
  exportBookings,
  ExportField,
} from '@/utils/export';
import { Booking, BookingStatus, ContainerType } from '@/types/booking';

// ── Mocks ─────────────────────────────────────────────────────────────────────

const mockSaveAs = jest.fn();
jest.mock('file-saver', () => ({
  saveAs: (...args: unknown[]) => mockSaveAs(...args),
}));

const mockAoaToSheet = jest.fn().mockReturnValue({ '!ref': 'A1:K2' });
const mockBookNew = jest.fn().mockReturnValue({});
const mockBookAppendSheet = jest.fn();
const mockWrite = jest.fn().mockReturnValue(new ArrayBuffer(8));

jest.mock('xlsx', () => ({
  utils: {
    aoa_to_sheet: (...args: unknown[]) => mockAoaToSheet(...args),
    book_new: () => mockBookNew(),
    book_append_sheet: (...args: unknown[]) => mockBookAppendSheet(...args),
  },
  write: (...args: unknown[]) => mockWrite(...args),
}));

// ── Blob capture helper ────────────────────────────────────────────────────────
// blob.text() is not available in all jsdom versions; instead we intercept the
// Blob constructor to capture the raw string before it's wrapped.

const OriginalBlob = global.Blob;
let capturedBlobParts: string[] = [];

beforeEach(() => {
  jest.clearAllMocks();
  capturedBlobParts = [];

  global.Blob = jest.fn().mockImplementation((parts?: BlobPart[], options?: BlobPropertyBag) => {
    const content = (parts ?? []).map(p => (typeof p === 'string' ? p : '')).join('');
    capturedBlobParts.push(content);
    return { type: options?.type ?? '', size: content.length } as Blob;
  }) as unknown as typeof Blob;
});

afterEach(() => {
  global.Blob = OriginalBlob;
});

// ── Fixtures ──────────────────────────────────────────────────────────────────

const makeBooking = (overrides: Partial<Booking> = {}): Booking => ({
  id: 'b-1',
  bookingNumber: 'WCM-2024-ABC001',
  status: BookingStatus.CONFIRMED,
  shipper: {
    name: 'Acme Corp',
    street: '1 rue de la Paix',
    city: 'Paris',
    postalCode: '75001',
    country: 'France',
  },
  consignee: {
    name: 'Beta Ltd',
    street: '42 Main St',
    city: 'Shanghai',
    postalCode: '200000',
    country: 'China',
  },
  containers: [
    { id: 'c-1', type: ContainerType.DRY_40 },
    { id: 'c-2', type: ContainerType.HIGH_CUBE_40 },
  ],
  rateQuote: {
    id: 'rq-1',
    carrierName: 'Maersk',
    carrierScac: 'MAEU',
    origin: 'Le Havre',
    destination: 'Shanghai',
    priceValue: 2500,
    priceCurrency: 'USD',
    etd: '2024-03-01T00:00:00Z',
    eta: '2024-04-01T00:00:00Z',
    transitDays: 31,
  },
  createdAt: '2024-01-15T10:00:00Z',
  updatedAt: '2024-01-15T10:00:00Z',
  ...overrides,
});

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('exportToCSV', () => {
  it('calls saveAs once', () => {
    exportToCSV([makeBooking()]);
    expect(mockSaveAs).toHaveBeenCalledTimes(1);
  });

  it('passes a Blob as the first saveAs argument', () => {
    exportToCSV([makeBooking()]);
    const [blob] = mockSaveAs.mock.calls[0];
    expect(blob).toBeDefined();
    expect(blob.type).toContain('text/csv');
  });

  it('uses the default filename', () => {
    exportToCSV([makeBooking()]);
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.csv');
  });

  it('uses a custom filename when provided', () => {
    exportToCSV([makeBooking()], undefined, 'my-export.csv');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('my-export.csv');
  });

  it('generates a CSV header with default field labels', () => {
    exportToCSV([makeBooking()]);
    const csv = capturedBlobParts[0];
    expect(csv).toContain('Booking Number');
    expect(csv).toContain('Status');
    expect(csv).toContain('Carrier');
    expect(csv).toContain('Origin');
    expect(csv).toContain('Destination');
  });

  it('includes booking data in the CSV rows', () => {
    exportToCSV([makeBooking()]);
    const csv = capturedBlobParts[0];
    expect(csv).toContain('WCM-2024-ABC001');
    expect(csv).toContain('confirmed');
    expect(csv).toContain('Maersk');
    expect(csv).toContain('Le Havre');
    expect(csv).toContain('Shanghai');
  });

  it('applies custom fields and their labels', () => {
    const customFields: ExportField[] = [
      { key: 'bookingNumber', label: 'Number' },
      { key: 'status', label: 'State' },
    ];
    exportToCSV([makeBooking()], customFields);
    const csv = capturedBlobParts[0];
    expect(csv).toContain('Number');
    expect(csv).toContain('State');
    expect(csv).not.toContain('Carrier');
  });

  it('applies field formatters', () => {
    const customFields: ExportField[] = [
      { key: 'status', label: 'Status', formatter: (v: string) => v.toUpperCase() },
    ];
    exportToCSV([makeBooking()], customFields);
    expect(capturedBlobParts[0]).toContain('CONFIRMED');
  });

  it('extracts nested values with dot-notation keys', () => {
    const customFields: ExportField[] = [
      { key: 'rateQuote.carrierName', label: 'Carrier' },
      { key: 'shipper.name', label: 'Shipper' },
    ];
    exportToCSV([makeBooking()], customFields);
    const csv = capturedBlobParts[0];
    expect(csv).toContain('Maersk');
    expect(csv).toContain('Acme Corp');
  });

  it('extracts deeply nested values', () => {
    const customFields: ExportField[] = [{ key: 'consignee.city', label: 'Consignee City' }];
    exportToCSV([makeBooking()], customFields);
    expect(capturedBlobParts[0]).toContain('Shanghai');
  });

  it('generates only the header row when data is empty', () => {
    exportToCSV([]);
    const lines = capturedBlobParts[0].split('\n');
    expect(lines).toHaveLength(1);
  });

  it('generates one data row per booking', () => {
    exportToCSV([makeBooking(), makeBooking({ id: 'b-2', bookingNumber: 'WCM-2024-ABC002' })]);
    const lines = capturedBlobParts[0].trim().split('\n');
    expect(lines).toHaveLength(3); // header + 2 rows
  });

  it('wraps all cell values in double quotes', () => {
    const customFields: ExportField[] = [{ key: 'bookingNumber', label: 'Number' }];
    exportToCSV([makeBooking()], customFields);
    const dataLine = capturedBlobParts[0].split('\n')[1];
    expect(dataLine).toMatch(/^".*"$/);
  });

  it('escapes double quotes inside cell values', () => {
    const customFields: ExportField[] = [{ key: 'shipper.name', label: 'Shipper' }];
    const booking = makeBooking({
      shipper: {
        name: 'He said "hello"',
        street: '1 st',
        city: 'Paris',
        postalCode: '75001',
        country: 'France',
      },
    });
    exportToCSV([booking], customFields);
    // Original `"` should be escaped as `""`
    expect(capturedBlobParts[0]).toContain('He said ""hello""');
  });

  it('returns undefined', () => {
    expect(exportToCSV([makeBooking()])).toBeUndefined();
  });
});

describe('exportToExcel', () => {
  it('calls saveAs with the default filename', () => {
    exportToExcel([makeBooking()]);
    expect(mockSaveAs).toHaveBeenCalledTimes(1);
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.xlsx');
  });

  it('uses a custom filename', () => {
    exportToExcel([makeBooking()], undefined, 'report.xlsx');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('report.xlsx');
  });

  it('calls aoa_to_sheet with worksheet data', () => {
    exportToExcel([makeBooking()]);
    expect(mockAoaToSheet).toHaveBeenCalledTimes(1);
    const [wsData] = mockAoaToSheet.mock.calls[0];
    expect(Array.isArray(wsData[0])).toBe(true);
  });

  it('places the header labels in the first row', () => {
    exportToExcel([makeBooking()]);
    const [wsData] = mockAoaToSheet.mock.calls[0];
    const headers = wsData[0];
    expect(headers).toContain('Booking Number');
    expect(headers).toContain('Carrier');
    expect(headers).toContain('Status');
  });

  it('creates a new workbook', () => {
    exportToExcel([makeBooking()]);
    expect(mockBookNew).toHaveBeenCalledTimes(1);
  });

  it('appends the worksheet with the name "Bookings"', () => {
    exportToExcel([makeBooking()]);
    expect(mockBookAppendSheet).toHaveBeenCalledTimes(1);
    const [, , sheetName] = mockBookAppendSheet.mock.calls[0];
    expect(sheetName).toBe('Bookings');
  });

  it('calls XLSX.write with bookType "xlsx"', () => {
    exportToExcel([makeBooking()]);
    expect(mockWrite).toHaveBeenCalledTimes(1);
    const [, opts] = mockWrite.mock.calls[0];
    expect(opts.bookType).toBe('xlsx');
  });

  it('produces a row for each booking (plus one header)', () => {
    exportToExcel([makeBooking(), makeBooking({ id: 'b-2' })]);
    const [wsData] = mockAoaToSheet.mock.calls[0];
    expect(wsData).toHaveLength(3); // 1 header + 2 data rows
  });
});

describe('exportToJSON', () => {
  it('calls saveAs with the default filename', () => {
    exportToJSON([makeBooking()]);
    expect(mockSaveAs).toHaveBeenCalledTimes(1);
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.json');
  });

  it('uses a custom filename', () => {
    exportToJSON([makeBooking()], 'data.json');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('data.json');
  });

  it('creates a Blob with application/json type', () => {
    exportToJSON([makeBooking()]);
    const [blob] = mockSaveAs.mock.calls[0];
    expect(blob.type).toContain('application/json');
  });

  it('serialises bookings as valid JSON', () => {
    const booking = makeBooking();
    exportToJSON([booking]);
    const json = capturedBlobParts[0];
    const parsed = JSON.parse(json);
    expect(Array.isArray(parsed)).toBe(true);
    expect(parsed[0].bookingNumber).toBe('WCM-2024-ABC001');
  });

  it('produces pretty-printed JSON (2-space indent)', () => {
    exportToJSON([makeBooking()]);
    expect(capturedBlobParts[0]).toContain('\n  ');
  });
});

describe('exportBookings dispatcher', () => {
  it('routes "csv" to exportToCSV', () => {
    exportBookings([makeBooking()], 'csv');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.csv');
  });

  it('routes "excel" to exportToExcel', () => {
    exportBookings([makeBooking()], 'excel');
    expect(mockAoaToSheet).toHaveBeenCalledTimes(1);
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.xlsx');
  });

  it('routes "json" to exportToJSON', () => {
    exportBookings([makeBooking()], 'json');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('bookings-export.json');
  });

  it('throws for an unknown format', () => {
    expect(() => exportBookings([makeBooking()], 'pdf' as any)).toThrow(
      'Unsupported export format: pdf'
    );
  });

  it('passes a custom filename through to the underlying exporter', () => {
    exportBookings([makeBooking()], 'csv', undefined, 'custom.csv');
    const [, filename] = mockSaveAs.mock.calls[0];
    expect(filename).toBe('custom.csv');
  });
});
