import { describe, expect, it } from 'vitest';
import type { BreakdownItem, CertificateKpis } from '@ema-platform/api';
import {
  DASH,
  defaultRange,
  deltaColor,
  expiryBands,
  expiryUrgency,
  formatBucket,
  formatDelta,
  formatNumber,
  formatPercent,
  officerLabel,
  optionsFrom,
  queryToSearchParams,
  searchParamsToQuery,
  sliceColor,
} from './report-format';

const certificates = (partial: Partial<CertificateKpis>): CertificateKpis => ({
  total: 0,
  active: 0,
  expired: 0,
  suspended: 0,
  expiringIn30: 0,
  expiringIn60: 0,
  expiringIn90: 0,
  missingCertificate: 0,
  ...partial,
});

const item = (key: string, count = 1): BreakdownItem => ({
  key,
  label: key,
  count,
  percentage: 0,
});

describe('formatNumber', () => {
  it('renders a dash for a figure the API had no answer for', () => {
    expect(formatNumber(null)).toBe(DASH);
    expect(formatNumber(undefined)).toBe(DASH);
    expect(formatNumber(Number.NaN)).toBe(DASH);
  });

  it('keeps a real zero', () => {
    expect(formatNumber(0)).toBe('0');
  });

  it('honours decimals and a suffix', () => {
    expect(formatNumber(12.345, { decimals: 2 })).toBe('12.35');
    expect(formatNumber(7, { suffix: ' GT' })).toBe('7 GT');
  });
});

describe('formatPercent / formatDelta', () => {
  it('distinguishes no answer from zero', () => {
    expect(formatPercent(null)).toBe(DASH);
    expect(formatPercent(0)).toBe('0.0%');
    expect(formatDelta(null)).toBe(DASH);
  });

  it('signs a positive change', () => {
    expect(formatDelta(12.5)).toBe('+12.5%');
    expect(formatDelta(-4)).toBe('-4.0%');
  });

  it('colours a flat or absent change neutrally', () => {
    expect(deltaColor(null)).toBe('gray');
    expect(deltaColor(0)).toBe('gray');
    expect(deltaColor(1)).toBe('teal');
    expect(deltaColor(-1)).toBe('red');
  });
});

describe('expiryBands', () => {
  it('differences the API cumulative counts into disjoint bands', () => {
    expect(
      expiryBands(
        certificates({ expiringIn30: 4, expiringIn60: 9, expiringIn90: 11 }),
      ),
    ).toEqual([
      { label: 'Within 30 days', count: 4 },
      { label: '31–60 days', count: 5 },
      { label: '61–90 days', count: 2 },
    ]);
  });

  it('never draws a negative bar if the counts are not monotonic', () => {
    const bands = expiryBands(
      certificates({ expiringIn30: 9, expiringIn60: 4, expiringIn90: 4 }),
    );
    expect(bands.every((band) => band.count >= 0)).toBe(true);
  });
});

describe('expiryUrgency', () => {
  it('escalates on the boundaries', () => {
    expect(expiryUrgency(0)).toBe('red');
    expect(expiryUrgency(7)).toBe('red');
    expect(expiryUrgency(8)).toBe('orange');
    expect(expiryUrgency(30)).toBe('orange');
    expect(expiryUrgency(31)).toBe('gray');
  });
});

describe('officerLabel', () => {
  it('spells out the unassigned bucket and shortens a uuid', () => {
    expect(officerLabel('UNASSIGNED')).toBe('Unassigned');
    expect(officerLabel('c8d0a151-91e9-433e-b221-db331480b10f')).toBe(
      'c8d0a151…',
    );
    expect(officerLabel('short')).toBe('short');
  });
});

describe('sliceColor', () => {
  it('mutes the bookkeeping slices and cycles the rest', () => {
    const muted = sliceColor(item('OTHER'), 0);
    expect(sliceColor(item('Unknown'), 3)).toBe(muted);
    expect(sliceColor(item('SEA_GOING'), 0)).not.toBe(muted);
  });

  it('is stable for a given position', () => {
    expect(sliceColor(item('A'), 2)).toBe(sliceColor(item('B'), 2));
  });
});

describe('formatBucket', () => {
  it('reads a month bucket as a month and a day bucket as a day', () => {
    expect(formatBucket('2026-03-01', 'MONTH')).toMatch(/2026/);
    expect(formatBucket('2026-03-04', 'DAY')).not.toMatch(/2026/);
  });

  it('passes an unparseable bucket through rather than printing NaN', () => {
    expect(formatBucket('not-a-date', 'MONTH')).toBe('not-a-date');
  });
});

describe('defaultRange', () => {
  it('spans the twelve months the API defaults to', () => {
    const [from, to] = defaultRange(new Date('2026-08-18T00:00:00Z'));
    expect(from.toISOString().slice(0, 10)).toBe('2025-08-18');
    expect(to.toISOString().slice(0, 10)).toBe('2026-08-18');
  });
});

describe('url round trip', () => {
  it('drops empty values so an untouched dashboard has a clean link', () => {
    const params = queryToSearchParams({
      search: '',
      category: [],
      topN: 15,
    });
    expect(params.toString()).toBe('topN=15');
  });

  it('restores the filter state a shared link carries', () => {
    const query = {
      from: '2026-01-01',
      to: '2026-08-18',
      granularity: 'WEEK' as const,
      status: ['REGISTERED' as const, 'SUSPENDED' as const],
      flagState: ['Ethiopia'],
      search: 'abay',
      topN: 20,
    };
    expect(searchParamsToQuery(queryToSearchParams(query))).toEqual(query);
  });

  it('ignores a hand-edited value the API would reject', () => {
    const query = searchParamsToQuery(
      new URLSearchParams('topN=abc&granularity=YEAR'),
    );
    expect(query.topN).toBeUndefined();
    expect(query.granularity).toBeUndefined();
  });
});

describe('optionsFrom', () => {
  it('offers the register values but not the bookkeeping slices', () => {
    expect(
      optionsFrom([item('Ethiopia'), item('Unknown'), item('OTHER')]),
    ).toEqual(['Ethiopia']);
    expect(optionsFrom(undefined)).toEqual([]);
  });
});
