import { BadRequestException } from '@nestjs/common';
import type { DataSource } from 'typeorm';

import { ClearanceMilestoneService } from './clearance-milestone.service';
import type { ClearanceMilestone } from './entities/clearance-milestone.entity';

type Status = 'PENDING' | 'COMPLETED' | 'SKIPPED';

/**
 * Risk assignment is gated on the T1 being closed (catalog order
 * T1_CLOSED → RISK_ASSIGNED): customs cannot rate cargo still under transit.
 */
function makeService(t1Status: Status | 'MISSING') {
  const rows = new Map<string, ClearanceMilestone>();
  if (t1Status !== 'MISSING') {
    rows.set('T1_CLOSED', { milestoneCode: 'T1_CLOSED', status: t1Status } as ClearanceMilestone);
  }
  const risk = { milestoneCode: 'RISK_ASSIGNED', status: 'PENDING' } as ClearanceMilestone;
  rows.set('RISK_ASSIGNED', risk);

  const repo = {
    findOne: jest.fn(({ where }: { where: { milestoneCode: string } }) =>
      Promise.resolve(rows.get(where.milestoneCode) ?? null),
    ),
    save: jest.fn((m: ClearanceMilestone) => Promise.resolve(m)),
  };
  const dataSource = { getRepository: () => repo } as unknown as DataSource;
  return { service: new ClearanceMilestoneService(dataSource), repo, risk };
}

describe('ClearanceMilestoneService.assignRisk', () => {
  it('rejects the assignment while the T1 is still open', async () => {
    const { service, repo } = makeService('PENDING');

    await expect(service.assignRisk('b-1', 'GREEN')).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('rejects the assignment when the booking has no T1_CLOSED milestone', async () => {
    const { service, repo } = makeService('MISSING');

    await expect(service.assignRisk('b-1', 'GREEN')).rejects.toBeInstanceOf(
      BadRequestException,
    );
    expect(repo.save).not.toHaveBeenCalled();
  });

  it('assigns the risk level once the T1 is closed', async () => {
    const { service, risk } = makeService('COMPLETED');

    const saved = await service.assignRisk('b-1', 'RED', 'user-1');

    expect(saved.status).toBe('COMPLETED');
    expect(saved.metadata?.riskLevel).toBe('RED');
    expect(risk.triggeredByUserId).toBe('user-1');
  });

  it('assigns the risk level when the T1 step was skipped', async () => {
    const { service } = makeService('SKIPPED');

    const saved = await service.assignRisk('b-1', 'YELLOW');

    expect(saved.status).toBe('COMPLETED');
    expect(saved.metadata?.riskLevel).toBe('YELLOW');
  });

  /**
   * The level is customer-visible and stays correctable until duty is advised,
   * so a changed level must leave a trail rather than overwrite the last one.
   */
  describe('risk history', () => {
    it('records the first assignment with no previous level', async () => {
      const { service } = makeService('COMPLETED');

      const saved = await service.assignRisk('b-1', 'RED', 'user-1', 'initial rating', 'Abebe K.');

      expect(saved.metadata?.riskHistory).toHaveLength(1);
      expect(saved.metadata?.riskHistory?.[0]).toMatchObject({
        level: 'RED',
        assignedByUserId: 'user-1',
        assignedBy: 'Abebe K.',
        note: 'initial rating',
      });
      expect(saved.metadata?.riskHistory?.[0]).not.toHaveProperty('previousLevel');
    });

    it('keeps the earlier decision when the level is reassigned', async () => {
      const { service } = makeService('COMPLETED');

      await service.assignRisk('b-1', 'RED', 'user-1', undefined, 'Abebe K.');
      const saved = await service.assignRisk('b-1', 'GREEN', 'user-2', 'downgraded', 'Sara M.');

      expect(saved.metadata?.riskLevel).toBe('GREEN');
      expect(saved.metadata?.riskHistory).toHaveLength(2);
      // The original RED decision survives, with who made it.
      expect(saved.metadata?.riskHistory?.[0]).toMatchObject({
        level: 'RED',
        assignedBy: 'Abebe K.',
      });
      expect(saved.metadata?.riskHistory?.[1]).toMatchObject({
        level: 'GREEN',
        previousLevel: 'RED',
        assignedByUserId: 'user-2',
        assignedBy: 'Sara M.',
        note: 'downgraded',
      });
    });

    it('keeps the whole chain across several reassignments, oldest first', async () => {
      const { service } = makeService('COMPLETED');

      await service.assignRisk('b-1', 'GREEN');
      await service.assignRisk('b-1', 'YELLOW');
      const saved = await service.assignRisk('b-1', 'RED');

      expect(saved.metadata?.riskHistory?.map((e) => e.level)).toEqual([
        'GREEN',
        'YELLOW',
        'RED',
      ]);
    });

    it('does not record a repeat of the level already assigned', async () => {
      const { service } = makeService('COMPLETED');

      await service.assignRisk('b-1', 'GREEN');
      const saved = await service.assignRisk('b-1', 'GREEN');

      expect(saved.metadata?.riskHistory).toHaveLength(1);
    });

    it('always leaves riskLevel equal to the last history entry', async () => {
      const { service } = makeService('COMPLETED');

      await service.assignRisk('b-1', 'RED');
      const saved = await service.assignRisk('b-1', 'YELLOW');

      const history = saved.metadata?.riskHistory ?? [];
      expect(saved.metadata?.riskLevel).toBe(history[history.length - 1]?.level);
    });
  });
});
