import { Test, type TestingModule } from '@nestjs/testing';
import { DestinationVehicleTypeService } from './destination-vehicle-type.service';
import { DestinationVehicleTypeRepository } from './destination-vehicle-type.repository';
import type { DestinationVehicleTypeEntity } from '../../entities/destination-vehicle-type.entity';

describe('DestinationVehicleTypeService', () => {
  let service: DestinationVehicleTypeService;
  let managerQuery: jest.Mock;
  let repo: {
    findByDestination: jest.Mock;
    findAllWithRelations: jest.Mock;
    save: jest.Mock;
    softDelete: jest.Mock;
    currentTenantId: jest.Mock;
    currentUserId: jest.Mock;
    manager: { query: jest.Mock };
  };

  const TENANT = 'tenant-1';
  const USER = 'user-1';
  const DEST = 'dest-1';

  beforeEach(async () => {
    managerQuery = jest.fn().mockResolvedValue([]);
    repo = {
      findByDestination: jest.fn(),
      findAllWithRelations: jest.fn(),
      save: jest.fn(),
      softDelete: jest.fn(),
      currentTenantId: jest.fn().mockReturnValue(TENANT),
      currentUserId: jest.fn().mockReturnValue(USER),
      manager: { query: managerQuery },
    };
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        DestinationVehicleTypeService,
        { provide: DestinationVehicleTypeRepository, useValue: repo },
      ],
    }).compile();
    service = module.get(DestinationVehicleTypeService);
  });

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

  describe('findByDestination — public API (consumed by FE dropdowns)', () => {
    it('returns { id, name }[] shape, not bare IDs', async () => {
      repo.findByDestination.mockResolvedValue([
        {
          vehicle_type_id: 'vt-1',
          vehicle_type: { name: 'Sedan' },
        } as DestinationVehicleTypeEntity,
        { vehicle_type_id: 'vt-2', vehicle_type: { name: 'SUV' } } as DestinationVehicleTypeEntity,
      ]);

      const result = await service.findByDestination('dest-1');

      expect(result).toEqual([
        { id: 'vt-1', name: 'Sedan' }, // sorted by name (localeCompare puts Sedan before SUV)
        { id: 'vt-2', name: 'SUV' },
      ]);
    });

    it('sorts results alphabetically by name', async () => {
      repo.findByDestination.mockResolvedValue([
        { vehicle_type_id: 'vt-3', vehicle_type: { name: 'Tempo' } },
        { vehicle_type_id: 'vt-1', vehicle_type: { name: 'Coach' } },
        { vehicle_type_id: 'vt-2', vehicle_type: { name: 'Minibus' } },
      ] as DestinationVehicleTypeEntity[]);

      const result = await service.findByDestination('dest-1');

      expect(result.map((r) => r.name)).toEqual(['Coach', 'Minibus', 'Tempo']);
    });

    it('returns empty array when destination has no mappings (forces FE to show empty dropdown — agreed UX)', async () => {
      repo.findByDestination.mockResolvedValue([]);

      const result = await service.findByDestination('unconfigured-dest');

      expect(result).toEqual([]);
    });

    it('falls back to an em-dash when the joined vehicle_type relation is missing (defensive)', async () => {
      repo.findByDestination.mockResolvedValue([
        { vehicle_type_id: 'vt-orphan', vehicle_type: null },
      ] as unknown as DestinationVehicleTypeEntity[]);

      const result = await service.findByDestination('dest-1');

      expect(result).toEqual([{ id: 'vt-orphan', name: '—' }]);
    });

    it('forwards the destination_id to the repository unchanged', async () => {
      repo.findByDestination.mockResolvedValue([]);

      await service.findByDestination('the-exact-destination-id');

      expect(repo.findByDestination).toHaveBeenCalledWith('the-exact-destination-id');
    });
  });

  // ──────────────────────────────────────────────────────────────────────
  // upsertForDestination — mapping changes cascade into transport_pricing.
  // We mock manager.query() so we can assert the right cascade SQL was
  // issued without spinning up Postgres.
  // ──────────────────────────────────────────────────────────────────────
  describe('upsertForDestination — cascade into transport_pricing', () => {
    it('no-op when the incoming set matches the existing set (no cascade SQL fired)', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm1', vehicle_type_id: 'vt-1' },
        { id: 'm2', vehicle_type_id: 'vt-2' },
      ] as DestinationVehicleTypeEntity[]);

      const result = await service.upsertForDestination({
        destination_id: DEST,
        vehicle_type_ids: ['vt-1', 'vt-2'],
      });

      expect(repo.save).not.toHaveBeenCalled();
      expect(repo.softDelete).not.toHaveBeenCalled();
      expect(managerQuery).not.toHaveBeenCalled();
      expect(result).toMatchObject({ added_count: 0, removed_count: 0 });
    });

    it('fires the INSERT cascade for newly added vehicle types', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm1', vehicle_type_id: 'vt-1' },
      ] as DestinationVehicleTypeEntity[]);

      await service.upsertForDestination({
        destination_id: DEST,
        vehicle_type_ids: ['vt-1', 'vt-NEW'],
      });

      // mapping row added
      expect(repo.save).toHaveBeenCalledWith({ destination_id: DEST, vehicle_type_id: 'vt-NEW' });
      // exactly one cascade INSERT (no removed ids)
      const insertCalls = managerQuery.mock.calls.filter((c) =>
        (c[0] as string).includes('INSERT INTO transport_pricing'),
      );
      expect(insertCalls).toHaveLength(1);
      expect(insertCalls[0][1]).toEqual([USER, ['vt-NEW'], TENANT, DEST]);
    });

    it('fires the soft-delete cascade for removed vehicle types (active PVT rows only)', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm1', vehicle_type_id: 'vt-OLD' },
      ] as DestinationVehicleTypeEntity[]);

      await service.upsertForDestination({
        destination_id: DEST,
        vehicle_type_ids: [],
      });

      expect(repo.softDelete).toHaveBeenCalledWith('m1');
      const updateCalls = managerQuery.mock.calls.filter((c) =>
        (c[0] as string).startsWith('UPDATE transport_pricing'),
      );
      expect(updateCalls).toHaveLength(1);
      expect(updateCalls[0][1]).toEqual([USER, TENANT, DEST, ['vt-OLD']]);
    });

    it('fires both cascades when the same upsert adds + removes', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm-keep', vehicle_type_id: 'vt-keep' },
        { id: 'm-drop', vehicle_type_id: 'vt-drop' },
      ] as DestinationVehicleTypeEntity[]);

      const result = await service.upsertForDestination({
        destination_id: DEST,
        vehicle_type_ids: ['vt-keep', 'vt-add'],
      });

      expect(repo.save).toHaveBeenCalledTimes(1); // vt-add only
      expect(repo.softDelete).toHaveBeenCalledTimes(1); // vt-drop only
      // The two cascade queries
      const sqls = managerQuery.mock.calls.map((c) => c[0] as string);
      expect(sqls.some((s) => s.startsWith('UPDATE transport_pricing'))).toBe(true);
      expect(sqls.some((s) => s.includes('INSERT INTO transport_pricing'))).toBe(true);
      expect(result).toMatchObject({ added_count: 1, removed_count: 1 });
    });

    it('SQL for added vehicle types only touches active transport_services for the destination', async () => {
      repo.findByDestination.mockResolvedValue([]);
      await service.upsertForDestination({
        destination_id: DEST,
        vehicle_type_ids: ['vt-NEW'],
      });

      const sql = managerQuery.mock.calls.find((c) =>
        (c[0] as string).includes('INSERT INTO transport_pricing'),
      )![0] as string;
      expect(sql).toMatch(/ts\.is_deleted\s*=\s*FALSE/);
      expect(sql).toMatch(/ts\.is_active\s*=\s*TRUE/);
      expect(sql).toMatch(/d\.currency_id IS NOT NULL/);
      // ON CONFLICT clause matches the partial-unique index predicate
      expect(sql).toMatch(/ON CONFLICT/);
      expect(sql).toMatch(/is_deleted = FALSE AND mode = 'PVT'/);
      expect(sql).toMatch(/DO NOTHING/);
    });

    it('SQL for removed vehicle types only touches active rows (does not clobber soft-deleted history)', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm1', vehicle_type_id: 'vt-OLD' },
      ] as DestinationVehicleTypeEntity[]);
      await service.upsertForDestination({ destination_id: DEST, vehicle_type_ids: [] });

      const sql = managerQuery.mock.calls.find((c) =>
        (c[0] as string).startsWith('UPDATE transport_pricing'),
      )![0] as string;
      expect(sql).toMatch(/is_deleted\s*=\s*FALSE/);
      expect(sql).toMatch(/vehicle_type_id\s*=\s*ANY/);
    });
  });

  // ──────────────────────────────────────────────────────────────────────
  // removeByDestination — wholesale clear cascades into pricing too.
  // ──────────────────────────────────────────────────────────────────────
  describe('removeByDestination — cascade', () => {
    it('soft-deletes mapping rows AND fires the pricing-removal cascade', async () => {
      repo.findByDestination.mockResolvedValue([
        { id: 'm1', vehicle_type_id: 'vt-1' },
        { id: 'm2', vehicle_type_id: 'vt-2' },
      ] as DestinationVehicleTypeEntity[]);

      const result = await service.removeByDestination(DEST);

      expect(repo.softDelete).toHaveBeenCalledTimes(2);
      // Should fire ONE cascade UPDATE listing both removed IDs (no INSERT)
      expect(managerQuery).toHaveBeenCalledTimes(1);
      expect(managerQuery.mock.calls[0][0] as string).toMatch(/^UPDATE transport_pricing/);
      expect(managerQuery.mock.calls[0][1]).toEqual([USER, TENANT, DEST, ['vt-1', 'vt-2']]);
      expect(result.removed).toBe(2);
    });

    it('no cascade SQL fires when the destination already had no mappings', async () => {
      repo.findByDestination.mockResolvedValue([]);

      const result = await service.removeByDestination(DEST);

      expect(repo.softDelete).not.toHaveBeenCalled();
      expect(managerQuery).not.toHaveBeenCalled();
      expect(result.removed).toBe(0);
    });
  });
});
