/**
 * F-146 — ReportingService unit tests (mocked DB).
 *
 * Verifies ReportingService.summary() and ReportingService.sales() return
 * correct DTOs, apply filters correctly, and handle empty data gracefully.
 */

import { describe, expect, it, vi } from 'vitest';
import type pg from 'pg';
import { ReportingService } from '../application/reporting-service.js';
import type { ReportingFilters } from '../domain/filters.js';

function makeFilters(overrides: Partial<ReportingFilters> = {}): ReportingFilters {
  return {
    range: { from: '2026-08-01T00:00:00Z', to: '2026-08-31T23:59:59Z' },
    compare: 'none',
    channel: 'all',
    storeIds: [],
    terminalIds: [],
    cashierIds: [],
    paymentMethodIds: [],
    productIds: [],
    categoryIds: [],
    brandIds: [],
    customerId: undefined,
    state: [],
    groupBy: undefined,
    page: 1,
    pageSize: 50,
    sort: undefined,
    ...overrides,
  };
}

function makeMockPool(rows: Record<string, unknown>[]) {
  return {
    query: vi.fn().mockResolvedValue({ rows }),
  } as unknown as pg.Pool;
}

describe('ReportingService', () => {
  // ── summary() ───────────────────────────────────────────────────────────

  describe('summary()', () => {
    it('returns correct DTO structure', async () => {
      const pool = makeMockPool([{
        orders: 10,
        customers: 8,
        gross_sales_cents: '50000',
        discounts_cents: '2000',
        tax_cents: '4500',
        units_sold: '15',
        shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.summary(makeFilters());

      expect(result).toMatchObject({
        range: { from: '2026-08-01T00:00:00Z', to: '2026-08-31T23:59:59Z' },
        filters: { channel: 'all', storeIds: [], terminalIds: [] },
        comparison: null,
        dataAvailability: {
          grossSales: 'available',
          netSales: 'unavailable',
          discounts: 'available',
          tax: 'available',
          unitsSold: 'available',
          orders: 'available',
          customers: 'available',
          margin: 'unavailable',
          paymentMethod: 'unavailable',
          refunds: 'unavailable',
          shipping: 'available',
        },
        totals: {
          orders: 10,
          customers: 8,
          grossSalesCents: 50000,
          discountsCents: 2000,
          taxCents: 4500,
          unitsSold: 15,
          shippingCents: 0,
        },
        updatedAt: expect.any(String),
        cache: { hit: false, maxAgeSeconds: 0 },
      });
    });

    it('handles empty result (zero metrics)', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      const result = await svc.summary(makeFilters());

      expect(result.totals).toEqual({
        orders: 0, customers: 0, grossSalesCents: 0,
        discountsCents: 0, taxCents: 0, unitsSold: 0, shippingCents: 0,
      });
    });

    it('applies channel filter', async () => {
      const pool = makeMockPool([{
        orders: 5, customers: 5, gross_sales_cents: '0',
        discounts_cents: '0', tax_cents: '0', units_sold: '0', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      await svc.summary(makeFilters({ channel: 'pos' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('AND ($4::text IS NULL OR o.source = $4)'),
        expect.arrayContaining(['pos']),
      );
    });

    it('applies storeIds filter', async () => {
      const storeId = '00000000-0000-0000-0000-000000000001';
      const pool = makeMockPool([{
        orders: 3, customers: 3, gross_sales_cents: '0',
        discounts_cents: '0', tax_cents: '0', units_sold: '0', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      await svc.summary(makeFilters({ storeIds: [storeId] }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('cardinality($5::uuid[])'),
        expect.arrayContaining([expect.arrayContaining([storeId])]),
      );
    });

    it('returns null comparison when compare=none', async () => {
      const pool = makeMockPool([{
        orders: 1, customers: 1, gross_sales_cents: '100',
        discounts_cents: '0', tax_cents: '0', units_sold: '1', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.summary(makeFilters({ compare: 'none' }));
      expect(result.comparison).toBeNull();
    });

    it('dataAvailability reflects model state', async () => {
      const pool = makeMockPool([{
        orders: 0, customers: 0, gross_sales_cents: '0',
        discounts_cents: '0', tax_cents: '0', units_sold: '0', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.summary(makeFilters());
      expect(result.dataAvailability.margin).toBe('unavailable');
      expect(result.dataAvailability.paymentMethod).toBe('unavailable');
      expect(result.dataAvailability.refunds).toBe('unavailable');
      expect(result.dataAvailability.netSales).toBe('unavailable');
      expect(result.dataAvailability.grossSales).toBe('available');
      expect(result.dataAvailability.shipping).toBe('available');
    });
  });

  // ── sales() ────────────────────────────────────────────────────────────

  describe('sales()', () => {
    it('returns correct DTO structure with pagination', async () => {
      const pool = makeMockPool([{
        period: '2026-08-01T00:00:00.000Z',
        channel: 'pos',
        store_id: null,
        terminal_id: null,
        orders: 5,
        customers: 4,
        gross_sales_cents: '30000',
        discounts_cents: '1000',
        tax_cents: '2700',
        units_sold: '8',
        shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'day' }));

      expect(result).toMatchObject({
        range: { from: '2026-08-01T00:00:00Z', to: '2026-08-31T23:59:59Z' },
        filters: {
          channel: 'all',
          storeIds: [],
          terminalIds: [],
          groupBy: 'day',
        },
        comparison: null,
        pagination: { page: 1, pageSize: 50, totalRows: 0 },
        updatedAt: expect.any(String),
      });
      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.period).toBeTruthy();
      expect(result.items[0]!.metrics.grossSalesCents).toBe(30000);
    });

    it('applies LIMIT and OFFSET for pagination', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      await svc.sales(makeFilters({ page: 3, pageSize: 25 }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('LIMIT $7 OFFSET $8'),
        expect.arrayContaining([25, 50]), // $7=pageSize=25, $8=offset=(3-1)*25=50
      );
    });

    it('handles empty rows gracefully', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'day' }));
      expect(result.items).toHaveLength(0);
    });

    it('returns totals row alongside grouped items', async () => {
      const pool = makeMockPool([
        {
          period: '2026-08-01T00:00:00.000Z', channel: 'pos',
          store_id: null, terminal_id: null,
          orders: 3, customers: 3, gross_sales_cents: '15000',
          discounts_cents: '500', tax_cents: '1350', units_sold: '4', shipping_cents: '0',
        },
      ]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'day' }));

      // totals comes from a separate summary query
      expect(result.totals).toBeDefined();
      expect(typeof result.totals.grossSalesCents).toBe('number');
    });

    it('applies channel filter in sales query', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      await svc.sales(makeFilters({ channel: 'ecommerce' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('AND ($4::text IS NULL OR o.source = $4)'),
        expect.arrayContaining(['ecommerce']),
      );
    });

    it('handles groupBy=month', async () => {
      const pool = makeMockPool([{
        period: '2026-08-01T00:00:00.000Z', channel: null,
        store_id: null, terminal_id: null,
        orders: 2, customers: 2, gross_sales_cents: '8000',
        discounts_cents: '0', tax_cents: '720', units_sold: '2', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'month' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining("DATE_TRUNC('month'"),
        expect.any(Array),
      );
      expect(result.items).toHaveLength(1);
    });

    it('handles groupBy=channel', async () => {
      const pool = makeMockPool([{
        period: null, channel: 'pos',
        store_id: null, terminal_id: null,
        orders: 4, customers: 4, gross_sales_cents: '20000',
        discounts_cents: '0', tax_cents: '1800', units_sold: '5', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'channel' }));

      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.channel).toBe('pos');
      expect(result.items[0]!.metrics.grossSalesCents).toBe(20000);
    });

    it('handles groupBy=store', async () => {
      const storeId = '00000000-0000-0000-0000-000000000001';
      const pool = makeMockPool([{
        period: null, channel: null,
        store_id: storeId, terminal_id: null,
        orders: 6, customers: 5, gross_sales_cents: '35000',
        discounts_cents: '1000', tax_cents: '3150', units_sold: '10', shipping_cents: '0',
      }]);
      const svc = new ReportingService(pool);
      const result = await svc.sales(makeFilters({ groupBy: 'store' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('o.store_id'),
        expect.any(Array),
      );
      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.storeId).toBe(storeId);
    });

    it('separates dimensional columns from aggregate metrics with a comma', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      await svc.sales(makeFilters({ groupBy: 'day' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringMatching(/NULL::uuid AS terminal_id,\s+COUNT\(DISTINCT o\.id\)/),
        expect.any(Array),
      );
    });

    it('groups terminal reports by both store and terminal', async () => {
      const pool = makeMockPool([]);
      const svc = new ReportingService(pool);
      await svc.sales(makeFilters({ groupBy: 'terminal' }));

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('GROUP BY store_id, terminal_id'),
        expect.any(Array),
      );
    });
  });
});
