import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { TaxSlabResolverService } from './tax-slab-resolver.service';
import { TaxSlabEntity } from '../../entities/tax-slab.entity';
import { TaxTypeEntity } from '../../entities/tax-type.entity';
import { TaxComponentEntity } from '../../entities/tax-component.entity';
import { DestinationEntity } from '../../entities/destination.entity';
import { TransportServiceEntity } from '../../entities/transport-service.entity';
import { ActivityEntity } from '../../entities/activity.entity';
import { SupplierEntity } from '../../entities/supplier.entity';

// ─── Helpers ────────────────────────────────────────────────────────────────

function makeMockRepo() {
  return {
    findOne: jest.fn(),
    find: jest.fn(),
  };
}

const DEST_ID = 'dest-1';
const SVC_ID = 'svc-1';
const ACT_ID = 'act-1';
const SUP_ID = 'sup-1';

const GST_SLAB = {
  id: 'slab-gst',
  tax_type_id: 'gst',
  total_rate: '18.00',
  component_rates: { 'cgst-comp': 9, 'sgst-comp': 9 },
  is_active: true,
  is_deleted: false,
} as unknown as TaxSlabEntity;

const VAT_SLAB = {
  id: 'slab-vat',
  tax_type_id: 'vat',
  total_rate: '5.00',
  component_rates: { 'vat-comp': 5 },
  is_active: true,
  is_deleted: false,
} as unknown as TaxSlabEntity;

const INACTIVE_SLAB = {
  id: 'slab-inactive',
  tax_type_id: 'gst',
  total_rate: '18.00',
  component_rates: { 'cgst-comp': 9, 'sgst-comp': 9 },
  is_active: false,
  is_deleted: false,
} as unknown as TaxSlabEntity;

const GST_TYPE = { id: 'gst', name: 'GST', is_deleted: false } as unknown as TaxTypeEntity;
const VAT_TYPE = { id: 'vat', name: 'VAT', is_deleted: false } as unknown as TaxTypeEntity;

const CGST_COMP = {
  id: 'cgst-comp',
  tax_type_id: 'gst',
  name: 'CGST',
  sort_order: 0,
  is_active: true,
} as unknown as TaxComponentEntity;
const SGST_COMP = {
  id: 'sgst-comp',
  tax_type_id: 'gst',
  name: 'SGST',
  sort_order: 1,
  is_active: true,
} as unknown as TaxComponentEntity;
const VAT_COMP = {
  id: 'vat-comp',
  tax_type_id: 'vat',
  name: 'VAT',
  sort_order: 0,
  is_active: true,
} as unknown as TaxComponentEntity;

// ─── Suite ──────────────────────────────────────────────────────────────────

describe('TaxSlabResolverService', () => {
  let service: TaxSlabResolverService;
  let slabRepo: ReturnType<typeof makeMockRepo>;
  let typeRepo: ReturnType<typeof makeMockRepo>;
  let compRepo: ReturnType<typeof makeMockRepo>;
  let destRepo: ReturnType<typeof makeMockRepo>;
  let transportRepo: ReturnType<typeof makeMockRepo>;
  let activityRepo: ReturnType<typeof makeMockRepo>;
  let supplierRepo: ReturnType<typeof makeMockRepo>;

  beforeEach(async () => {
    slabRepo = makeMockRepo();
    typeRepo = makeMockRepo();
    compRepo = makeMockRepo();
    destRepo = makeMockRepo();
    transportRepo = makeMockRepo();
    activityRepo = makeMockRepo();
    supplierRepo = makeMockRepo();

    // Default slabRepo.find to [] so the new rollupActiveDestinationSlabs
    // fallback in resolveForHotelOrSpecial doesn't crash tests that
    // only stub findOne — the fallback iterates the result array.
    slabRepo.find.mockResolvedValue([]);

    const module = await Test.createTestingModule({
      providers: [
        TaxSlabResolverService,
        { provide: getRepositoryToken(TaxSlabEntity), useValue: slabRepo },
        { provide: getRepositoryToken(TaxTypeEntity), useValue: typeRepo },
        { provide: getRepositoryToken(TaxComponentEntity), useValue: compRepo },
        { provide: getRepositoryToken(DestinationEntity), useValue: destRepo },
        { provide: getRepositoryToken(TransportServiceEntity), useValue: transportRepo },
        { provide: getRepositoryToken(ActivityEntity), useValue: activityRepo },
        { provide: getRepositoryToken(SupplierEntity), useValue: supplierRepo },
      ],
    }).compile();
    service = module.get(TaxSlabResolverService);
  });

  afterEach(() => jest.clearAllMocks());

  // ── loadSnapshot ──────────────────────────────────────────────────────────

  describe('loadSnapshot', () => {
    it('null/undefined slabId → null', async () => {
      expect(await service.loadSnapshot(null)).toBeNull();
      expect(await service.loadSnapshot(undefined)).toBeNull();
      expect(slabRepo.findOne).not.toHaveBeenCalled();
    });

    it('slab not found / soft-deleted / inactive → null', async () => {
      slabRepo.findOne.mockResolvedValue(null);
      expect(await service.loadSnapshot('missing')).toBeNull();
    });

    it('parent tax_type missing → null even if slab exists', async () => {
      slabRepo.findOne.mockResolvedValue(GST_SLAB);
      typeRepo.findOne.mockResolvedValue(null);
      expect(await service.loadSnapshot('slab-gst')).toBeNull();
    });

    it('returns slab + components in sort_order', async () => {
      slabRepo.findOne.mockResolvedValue(GST_SLAB);
      typeRepo.findOne.mockResolvedValue(GST_TYPE);
      // Intentionally return components out of order — service should
      // re-emit them sorted via the find's `order` clause; here we just
      // verify the mapper preserves the input order.
      compRepo.find.mockResolvedValue([CGST_COMP, SGST_COMP]);
      const snap = await service.loadSnapshot('slab-gst');
      expect(snap).toEqual({
        slab_id: 'slab-gst',
        slab_name: 'GST',
        total_rate: 18,
        components: [
          { name: 'CGST', rate: 9 },
          { name: 'SGST', rate: 9 },
        ],
      });
    });

    it('components without a rate in component_rates are filtered out', async () => {
      // Slab has CGST rate but no SGST entry (a partial config). The
      // snapshot should omit SGST so the helper doesn't try to split
      // tax across an unrated component.
      const partial = {
        ...GST_SLAB,
        component_rates: { 'cgst-comp': 9 },
        total_rate: '9.00',
      } as unknown as TaxSlabEntity;
      slabRepo.findOne.mockResolvedValue(partial);
      typeRepo.findOne.mockResolvedValue(GST_TYPE);
      compRepo.find.mockResolvedValue([CGST_COMP, SGST_COMP]);
      const snap = await service.loadSnapshot('slab-gst');
      expect(snap?.components).toEqual([{ name: 'CGST', rate: 9 }]);
    });

    it('total_rate stored as decimal string is coerced to number', async () => {
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);
      const snap = await service.loadSnapshot('slab-vat');
      expect(typeof snap?.total_rate).toBe('number');
      expect(snap?.total_rate).toBe(5);
    });
  });

  // ── Hierarchy: hotel + special ────────────────────────────────────────────

  describe('resolveForHotelOrSpecial', () => {
    it('uses destination slab when set', async () => {
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);
      const snap = await service.resolveForHotelOrSpecial(DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
    });

    it('destination has no slab → null', async () => {
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: null });
      const snap = await service.resolveForHotelOrSpecial(DEST_ID);
      expect(snap).toBeNull();
      expect(slabRepo.findOne).not.toHaveBeenCalled();
    });

    it('destination not found → null', async () => {
      destRepo.findOne.mockResolvedValue(null);
      const snap = await service.resolveForHotelOrSpecial(DEST_ID);
      expect(snap).toBeNull();
    });

    // ─── Audit fix #11 (2026-06-13) — fallback to active destination slabs
    // when destinations.tax_slab_id is NULL. Without this, lines saved
    // by tenants that never "promoted" a slab as the destination default
    // silently land with tax_snapshot = null.
    it('falls back to single active destination slab when tax_slab_id is NULL', async () => {
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: null });
      slabRepo.find.mockResolvedValue([VAT_SLAB]);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);
      const snap = await service.resolveForHotelOrSpecial(DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
      expect(snap?.slab_name).toBe('VAT');
      expect(snap?.total_rate).toBe(5);
      expect(snap?.components).toEqual([{ name: 'VAT', rate: 5 }]);
    });

    it('falls back to MERGED snapshot when destination has multiple active slabs', async () => {
      // E.g. Dubai with VAT 5% + Tourism Fee 7% — both must contribute
      // to the bifurcation render. slab_id is null because the merged
      // snapshot is no longer a single-slab reference.
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: null });
      slabRepo.find.mockResolvedValue([GST_SLAB, VAT_SLAB]);
      typeRepo.findOne
        .mockResolvedValueOnce(GST_TYPE)
        .mockResolvedValueOnce(VAT_TYPE);
      compRepo.find
        .mockResolvedValueOnce([CGST_COMP, SGST_COMP])
        .mockResolvedValueOnce([VAT_COMP]);
      const snap = await service.resolveForHotelOrSpecial(DEST_ID);
      expect(snap?.slab_id).toBeNull();
      expect(snap?.slab_name).toBe('GST + VAT');
      expect(snap?.total_rate).toBe(23);
      expect(snap?.components).toEqual([
        { name: 'CGST', rate: 9 },
        { name: 'SGST', rate: 9 },
        { name: 'VAT', rate: 5 },
      ]);
    });
  });

  // ── Hierarchy: transport ──────────────────────────────────────────────────

  describe('resolveForTransport', () => {
    it('service-master override wins over destination default', async () => {
      transportRepo.findOne.mockResolvedValue({ id: SVC_ID, tax_slab_id: 'slab-gst' });
      slabRepo.findOne.mockResolvedValueOnce(GST_SLAB);
      typeRepo.findOne.mockResolvedValueOnce(GST_TYPE);
      compRepo.find.mockResolvedValueOnce([CGST_COMP, SGST_COMP]);

      const snap = await service.resolveForTransport(SVC_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-gst');
      expect(snap?.slab_name).toBe('GST');
      // Destination lookup is NOT performed when the override resolves.
      expect(destRepo.findOne).not.toHaveBeenCalled();
    });

    it('service has no override → falls through to destination', async () => {
      transportRepo.findOne.mockResolvedValue({ id: SVC_ID, tax_slab_id: null });
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);

      const snap = await service.resolveForTransport(SVC_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
    });

    it('override slab is inactive → falls through to destination', async () => {
      // Service points at an inactive slab. loadSnapshot returns null
      // for it; we should NOT crash — fall through to the destination.
      transportRepo.findOne.mockResolvedValue({ id: SVC_ID, tax_slab_id: 'slab-inactive' });
      slabRepo.findOne
        .mockResolvedValueOnce(null) // first call: override slab not found (inactive filter)
        .mockResolvedValueOnce(VAT_SLAB); // second call: destination's slab
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);

      const snap = await service.resolveForTransport(SVC_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
    });

    it('no transport service id → goes straight to destination', async () => {
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);

      const snap = await service.resolveForTransport(null, DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
      expect(transportRepo.findOne).not.toHaveBeenCalled();
    });
  });

  // ── Hierarchy: activity ───────────────────────────────────────────────────

  describe('resolveForActivity', () => {
    it('activity override wins', async () => {
      activityRepo.findOne.mockResolvedValue({ id: ACT_ID, tax_slab_id: 'slab-gst' });
      slabRepo.findOne.mockResolvedValue(GST_SLAB);
      typeRepo.findOne.mockResolvedValue(GST_TYPE);
      compRepo.find.mockResolvedValue([CGST_COMP, SGST_COMP]);
      const snap = await service.resolveForActivity(ACT_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-gst');
      expect(destRepo.findOne).not.toHaveBeenCalled();
    });

    it('activity has no override → destination fallback', async () => {
      activityRepo.findOne.mockResolvedValue({ id: ACT_ID, tax_slab_id: null });
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);
      const snap = await service.resolveForActivity(ACT_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
    });
  });

  // ── Hierarchy: supplier ───────────────────────────────────────────────────

  describe('resolveForSupplier', () => {
    it('supplier override wins', async () => {
      supplierRepo.findOne.mockResolvedValue({ id: SUP_ID, tax_slab_id: 'slab-gst' });
      slabRepo.findOne.mockResolvedValue(GST_SLAB);
      typeRepo.findOne.mockResolvedValue(GST_TYPE);
      compRepo.find.mockResolvedValue([CGST_COMP, SGST_COMP]);
      const snap = await service.resolveForSupplier(SUP_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-gst');
    });

    it('supplier has no override → destination fallback', async () => {
      supplierRepo.findOne.mockResolvedValue({ id: SUP_ID, tax_slab_id: null });
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: 'slab-vat' });
      slabRepo.findOne.mockResolvedValue(VAT_SLAB);
      typeRepo.findOne.mockResolvedValue(VAT_TYPE);
      compRepo.find.mockResolvedValue([VAT_COMP]);
      const snap = await service.resolveForSupplier(SUP_ID, DEST_ID);
      expect(snap?.slab_id).toBe('slab-vat');
    });

    it('no supplier + destination has no slab → null (exempt)', async () => {
      destRepo.findOne.mockResolvedValue({ id: DEST_ID, tax_slab_id: null });
      const snap = await service.resolveForSupplier(null, DEST_ID);
      expect(snap).toBeNull();
    });
  });

  // ── Inactive slabs ────────────────────────────────────────────────────────

  describe('inactive slab handling', () => {
    it('the find filter on slabRepo.findOne includes is_active=true (proven by the call shape)', async () => {
      // Just assert the where clause is what loadSnapshot expects; the
      // filtering itself is what blocks inactive slabs from being
      // returned by the repository.
      slabRepo.findOne.mockResolvedValue(null);
      await service.loadSnapshot('any-id');
      expect(slabRepo.findOne).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            id: 'any-id',
            is_deleted: false,
            is_active: true,
          }),
        }),
      );
      // Sanity: also assert that an inactive fixture would be excluded
      // — the mock returning null mirrors the prod repo's behaviour for
      // INACTIVE_SLAB (which carries is_active=false). The fixture is
      // referenced so it isn't dead.
      expect(INACTIVE_SLAB.is_active).toBe(false);
    });
  });
});
