import { ConflictException } from '@nestjs/common';

import { RatesService } from './rates.service';
import type { Rate } from '../entities/rate.entity';

/**
 * One rate per lane + scope, whatever the unit.
 *
 * Pricing resolves a single rate for a (type, container type, leg) and then
 * applies whatever unit it carries — it has no way to choose between a
 * per-container and a per-wagon row for the same 20ft lane, and used to bill
 * whichever the database happened to return first. So the unit is NOT part of a
 * rate's identity: changing how a lane is billed means editing its rate.
 */
describe('RatesService — one rate per pattern', () => {
  const DJ = '11111111-1111-4000-8000-000000000001';
  const ET = '11111111-1111-4000-8000-000000000002';
  const CT20 = '11111111-1111-4000-8000-000000000003';

  const existing = (over: Partial<Rate> = {}): Rate =>
    ({
      id: 'rate-existing',
      rateType: 'CONTAINER_IMPORT',
      rateUnit: 'PER_WAGON',
      rateValue: 1690,
      containerTypeId: CT20,
      cargoTypeId: null,
      tradeDirection: 'IMPORT',
      originYardId: DJ,
      destinationYardId: ET,
      status: 'LIVE',
      ...over,
    }) as Rate;

  const dto = {
    appliesTo: 'CONTAINER',
    trigger: 'ALWAYS',
    tradeDirection: 'IMPORT',
    containerTypeId: CT20,
    originYardId: DJ,
    destinationYardId: ET,
    rateValue: 845,
    rateUnit: 'PER_CONTAINER',
  };

  let repository: { findByPattern: jest.Mock; create: jest.Mock };
  let service: RatesService;

  beforeEach(() => {
    repository = {
      findByPattern: jest.fn().mockResolvedValue(null),
      create: jest.fn(async (r) => ({ id: 'rate-new', ...r })),
    };
    service = new RatesService(
      repository as never,
      {
        findById: jest.fn(async (id: string) => ({
          id,
          country: id === DJ ? 'Djibouti' : 'Ethiopia',
          label: id === DJ ? 'Doraleh' : 'Gelan',
        })),
      } as never,
      { findById: jest.fn().mockResolvedValue(null) } as never,
    );
  });

  it('refuses a second rate on the same lane that only differs by unit', async () => {
    repository.findByPattern.mockResolvedValue(existing());

    await expect(service.create(dto as never, 'staff-1')).rejects.toBeInstanceOf(
      ConflictException,
    );
    expect(repository.create).not.toHaveBeenCalled();
  });

  it('looks the pattern up without the unit, so either order collides', async () => {
    await service.create(dto as never, 'staff-1');

    const pattern = repository.findByPattern.mock.calls[0][0];
    expect(pattern).not.toHaveProperty('rateUnit');
    expect(pattern).toMatchObject({
      rateType: 'CONTAINER_IMPORT',
      containerTypeId: CT20,
      originYardId: DJ,
      destinationYardId: ET,
    });
  });

  it('still allows the same unit on a different lane', async () => {
    await service.create(dto as never, 'staff-1');
    expect(repository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        rateUnit: 'PER_CONTAINER',
        rateValue: 845,
        status: 'DRAFT',
      }),
    );
  });

  /**
   * Additive surcharges are billed per matching rate, each by its own unit, so
   * hazard is legitimately per-container for boxes AND per-ton for bulk. The
   * unit stays part of their identity or the second one could never be created.
   */
  it('keeps the unit in the key for an additive surcharge', async () => {
    await service.create(
      {
        appliesTo: 'OTHER',
        trigger: 'HAZARDOUS',
        rateValue: 300,
        rateUnit: 'PER_CONTAINER',
      } as never,
      'staff-1',
    );

    expect(repository.findByPattern.mock.calls[0][0]).toMatchObject({
      rateType: 'HAZARD_SURCHARGE',
      rateUnit: 'PER_CONTAINER',
    });
  });

  it('treats lashing as singly resolved — one unit per direction', async () => {
    await service.create(
      {
        appliesTo: 'OTHER',
        trigger: 'LASHING',
        tradeDirection: 'IMPORT',
        rateValue: 40,
        rateUnit: 'PER_TON',
      } as never,
      'staff-1',
    );

    expect(repository.findByPattern.mock.calls[0][0]).not.toHaveProperty(
      'rateUnit',
    );
  });
});
