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

import { ContractClearanceService } from './contract-clearance.service';
import type { Contract } from './entities/contract.entity';

/**
 * The duty advice → dispute → re-advice loop. GL Ethiopia advises an amount;
 * the customer either pays it or sends it back with a reason. Sending it back
 * reopens the advice milestone — that is what puts the Duty & tax step back in
 * GL's hands — and the round can repeat until the amount is agreed.
 */
describe('ContractClearanceService — duty dispute', () => {
  const contract = (over: Partial<Contract> = {}): Contract =>
    ({
      id: 'ctr-1',
      reference: 'CTR-2026-00042',
      tradeDirection: 'IMPORT',
      customsClearingEnabled: true,
      contractKind: 'ONE_TIME',
      ...over,
    }) as Contract;

  const milestone = (code: string, status: string) =>
    ({ milestoneCode: code, status }) as never;

  let repo: {
    currentCycle: jest.Mock;
    createReviewNote: jest.Mock;
    updateCycle: jest.Mock;
    findReviewNotes: jest.Mock;
  };
  let contractsService: { findById: jest.Mock };
  let workflowService: { listMilestones: jest.Mock };
  let milestoneService: { reopenForContract: jest.Mock };
  let notifier: { dutyDisputed: jest.Mock };
  let service: ContractClearanceService;

  const build = (milestones: unknown[]) => {
    workflowService.listMilestones.mockResolvedValue(milestones);
  };

  beforeEach(() => {
    repo = {
      currentCycle: jest.fn().mockResolvedValue({ id: 'cyc-1', dutyRequired: true }),
      createReviewNote: jest.fn().mockResolvedValue(undefined),
      updateCycle: jest.fn().mockResolvedValue(undefined),
      findReviewNotes: jest.fn().mockResolvedValue([]),
    };
    contractsService = { findById: jest.fn().mockResolvedValue(contract()) };
    workflowService = { listMilestones: jest.fn().mockResolvedValue([]) };
    milestoneService = { reopenForContract: jest.fn().mockResolvedValue(undefined) };
    notifier = { dutyDisputed: jest.fn() };

    service = new ContractClearanceService(
      repo as never,
      contractsService as never,
      {} as never, // bookingsService
      {} as never, // filesService
      {} as never, // fileUploadSettingsService
      workflowService as never,
      milestoneService as never,
      {} as never, // dropdownSettingsService
      {} as never, // glOperationsService
      notifier as never,
    );
    build([
      milestone('DUTY_TAXES_ADVISED', 'COMPLETED'),
      milestone('DUTY_TAX_PAID', 'PENDING'),
    ]);
  });

  it('records the objection and hands the step back to GL', async () => {
    await service.disputeDuty('ctr-1', '  Declared value is wrong  ', 'user-1');

    expect(repo.createReviewNote).toHaveBeenCalledWith(
      'ctr-1',
      'Declared value is wrong',
      'DUTY_DISPUTE',
      'user-1',
      'CUSTOMER',
    );
    // Reopening the advice milestone is what re-arms the Duty & tax step.
    expect(milestoneService.reopenForContract).toHaveBeenCalledWith(
      'ctr-1',
      'DUTY_TAXES_ADVISED',
    );
    expect(repo.updateCycle).toHaveBeenCalledWith('cyc-1', {
      currentPhase: 'GL_ET_OUTPUT',
    });
  });

  it('tells GL Ethiopia, not the customer', async () => {
    await service.disputeDuty('ctr-1', 'Too high', 'user-1');
    expect(notifier.dutyDisputed).toHaveBeenCalledWith(
      expect.objectContaining({ id: 'ctr-1' }),
      'Too high',
    );
  });

  it('requires a reason — GL cannot correct an unexplained objection', async () => {
    await expect(service.disputeDuty('ctr-1', '   ')).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(milestoneService.reopenForContract).not.toHaveBeenCalled();
  });

  it('refuses when nothing has been advised yet', async () => {
    build([milestone('DUTY_TAXES_ADVISED', 'PENDING')]);
    await expect(service.disputeDuty('ctr-1', 'Too high')).rejects.toThrow(
      /no advised duty amount/i,
    );
  });

  it('refuses once the payment slip is in — that is a refund, not a re-advice', async () => {
    build([
      milestone('DUTY_TAXES_ADVISED', 'COMPLETED'),
      milestone('DUTY_TAX_PAID', 'COMPLETED'),
    ]);
    await expect(service.disputeDuty('ctr-1', 'Too high')).rejects.toThrow(
      /already been submitted/i,
    );
  });

  it('refuses when duty was never required for this clearance', async () => {
    repo.currentCycle.mockResolvedValue({ id: 'cyc-1', dutyRequired: false });
    await expect(service.disputeDuty('ctr-1', 'Too high')).rejects.toThrow(
      /not required/i,
    );
  });

  describe('the view', () => {
    const buildDispute = (milestones: unknown[]) =>
      (
        service as unknown as {
          buildDutyDispute: (id: string, m: unknown[]) => Promise<unknown>;
        }
      ).buildDutyDispute('ctr-1', milestones);

    it('shows the objection while GL still owes a corrected advice', async () => {
      repo.findReviewNotes.mockResolvedValue([
        { body: 'Second look please', createdAt: new Date('2026-07-20T09:00:00Z') },
        { body: 'First objection', createdAt: new Date('2026-07-18T09:00:00Z') },
      ]);

      const dispute = await buildDispute([
        milestone('DUTY_TAXES_ADVISED', 'PENDING'),
      ]);

      expect(dispute).toMatchObject({ note: 'Second look please', rounds: 2 });
    });

    it('clears itself once GL re-advises', async () => {
      repo.findReviewNotes.mockResolvedValue([
        { body: 'First objection', createdAt: new Date('2026-07-18T09:00:00Z') },
      ]);

      const dispute = await buildDispute([
        milestone('DUTY_TAXES_ADVISED', 'COMPLETED'),
      ]);

      expect(dispute).toBeNull();
    });
  });
});
