import {
  areArchNeighbors,
  sortInArchOrder,
  FDI_TOOTH_IDS,
  isFdiTooth,
  sameArch,
  teethBetweenInclusive,
  toFdi,
} from './fdi';

describe('FDI geometry', () => {
  describe('toFdi — the patient-right convention', () => {
    // A mirrored quadrant produces a *valid* code for the wrong tooth, so no schema
    // check can catch it. These four cases are the guard.
    it('maps upper + patient right to quadrant 1', () => {
      expect(toFdi('upper', 'patient_right', 6)).toBe('16');
      expect(toFdi('upper', 'patient_right', 1)).toBe('11');
    });

    it('maps upper + patient left to quadrant 2', () => {
      expect(toFdi('upper', 'patient_left', 6)).toBe('26');
      expect(toFdi('upper', 'patient_left', 8)).toBe('28');
    });

    it('maps lower + patient left to quadrant 3', () => {
      expect(toFdi('lower', 'patient_left', 6)).toBe('36');
    });

    it('maps lower + patient right to quadrant 4', () => {
      expect(toFdi('lower', 'patient_right', 6)).toBe('46');
      expect(toFdi('lower', 'patient_right', 8)).toBe('48');
    });

    it('never clamps an out-of-range position', () => {
      expect(toFdi('upper', 'patient_right', 9)).toBeNull();
      expect(toFdi('upper', 'patient_right', 0)).toBeNull();
      expect(toFdi('upper', 'patient_right', -1)).toBeNull();
      expect(toFdi('upper', 'patient_right', 1.5)).toBeNull();
      expect(toFdi('upper', 'patient_right', Number.NaN)).toBeNull();
    });
  });

  describe('isFdiTooth', () => {
    it('accepts all 32 permanent teeth', () => {
      expect(FDI_TOOTH_IDS.size).toBe(32);
      for (const tooth of FDI_TOOTH_IDS) expect(isFdiTooth(tooth)).toBe(true);
    });

    it('rejects deciduous teeth — the chart has no primary dentition', () => {
      for (const tooth of ['51', '55', '61', '71', '85']) {
        expect(isFdiTooth(tooth)).toBe(false);
      }
    });

    it('rejects garbage', () => {
      for (const value of [
        '',
        '1',
        '19',
        '10',
        '29',
        '99',
        14,
        null,
        undefined,
        {},
      ]) {
        expect(isFdiTooth(value)).toBe(false);
      }
    });
  });

  describe('adjacency', () => {
    it('treats neighbours within a quadrant as adjacent', () => {
      expect(areArchNeighbors('14', '15')).toBe(true);
      expect(areArchNeighbors('15', '14')).toBe(true);
    });

    it('treats the midline pairs as adjacent', () => {
      expect(areArchNeighbors('11', '21')).toBe(true);
      expect(areArchNeighbors('41', '31')).toBe(true);
    });

    it('rejects non-neighbours and cross-arch pairs', () => {
      expect(areArchNeighbors('14', '16')).toBe(false);
      expect(areArchNeighbors('18', '28')).toBe(false);
      expect(areArchNeighbors('14', '44')).toBe(false);
      expect(areArchNeighbors('14', '14')).toBe(false);
    });
  });

  describe('sameArch', () => {
    it('groups by arch, not by quadrant', () => {
      expect(sameArch('18', '28')).toBe(true);
      expect(sameArch('48', '38')).toBe(true);
      expect(sameArch('18', '48')).toBe(false);
      expect(sameArch('14', '99')).toBe(false);
    });
  });

  describe('teethBetweenInclusive', () => {
    it('returns the span in arch order regardless of argument order', () => {
      expect(teethBetweenInclusive('14', '16')).toEqual(['16', '15', '14']);
      expect(teethBetweenInclusive('16', '14')).toEqual(['16', '15', '14']);
    });

    it('spans the midline', () => {
      expect(teethBetweenInclusive('12', '22')).toEqual([
        '12',
        '11',
        '21',
        '22',
      ]);
    });

    it('returns a single tooth for identical endpoints', () => {
      expect(teethBetweenInclusive('14', '14')).toEqual(['14']);
    });

    it('returns null across arches or for unknown teeth', () => {
      expect(teethBetweenInclusive('14', '44')).toBeNull();
      expect(teethBetweenInclusive('14', '99')).toBeNull();
      expect(teethBetweenInclusive('99', '14')).toBeNull();
    });
  });
  describe('sortInArchOrder', () => {
    it('sorts along the arch rather than lexically', () => {
      expect(sortInArchOrder(['14', '16', '15'])).toEqual(['16', '15', '14']);
    });

    it('places 11 beside 21 across the midline', () => {
      expect(sortInArchOrder(['21', '11', '12'])).toEqual(['12', '11', '21']);
    });

    it('is a no-op for an empty or single-tooth list', () => {
      expect(sortInArchOrder([])).toEqual([]);
      expect(sortInArchOrder(['14'])).toEqual(['14']);
    });

    it('does not drop teeth it cannot place', () => {
      expect(sortInArchOrder(['99', '14']).sort()).toEqual(['14', '99']);
    });
  });
});
