import {
  toVoiceIntent,
  VOICE_INTENT_JSON_SCHEMA,
  type WireVoiceIntent,
} from './extraction.wire';

const emptyDue = {
  kind: 'none' as const,
  weekday: null,
  which: null,
  unit: null,
  amount: null,
  jy: null,
  jm: null,
  jd: null,
  y: null,
  m: null,
  d: null,
};

const wire = (overrides: Partial<WireVoiceIntent> = {}): WireVoiceIntent => ({
  treatmentType: null,
  teeth: [],
  connectedSpans: [],
  comment: null,
  prosthesisDefaultType: null,
  prosthesisOverrides: [],
  labId: null,
  labMatchExact: false,
  due: emptyDue,
  ...overrides,
});

const positionalTooth = {
  spoken: 'شش بالا راست',
  fdi: null,
  arch: 'upper' as const,
  side: 'patient_right' as const,
  position: 6,
};

describe('VOICE_INTENT_JSON_SCHEMA', () => {
  it('is strict — every property required, no extras', () => {
    expect(VOICE_INTENT_JSON_SCHEMA.additionalProperties).toBe(false);
    expect([...VOICE_INTENT_JSON_SCHEMA.required].sort()).toEqual(
      Object.keys(VOICE_INTENT_JSON_SCHEMA.properties).sort(),
    );
  });

  it('requires every field of the due object, since strict mode allows no optionals', () => {
    const due = VOICE_INTENT_JSON_SCHEMA.properties.due;
    expect([...due.required].sort()).toEqual(
      Object.keys(due.properties).sort(),
    );
  });
});

describe('toVoiceIntent', () => {
  it('takes the explicit branch for a code the model wrote in Persian digits', () => {
    // The model is reading Persian text back, so "۲۶" and a digit-by-digit "2 6" both
    // reach us. Matching only ASCII drops the tooth into the positional branch with no
    // quadrant, where it is reported as unresolved — the clinician loses a tooth and is
    // told the words were the problem.
    for (const raw of ['\u06F2\u06F6', '2 6', ' 26 ', '\u0662\u0666']) {
      const [tooth] = toVoiceIntent(
        wire({
          teeth: [
            {
              spoken: '\u0628\u06CC\u0633\u062A \u0648 \u0634\u0634',
              fdi: raw,
              arch: null,
              side: null,
              position: null,
            },
          ],
        }),
      ).teeth;
      expect(tooth).toEqual({
        kind: 'explicit',
        fdi: '26',
        spoken: '\u0628\u06CC\u0633\u062A \u0648 \u0634\u0634',
      });
    }
  });

  it('narrows a positional tooth', () => {
    const result = toVoiceIntent(wire({ teeth: [positionalTooth] }));
    expect(result.teeth[0]).toEqual({
      kind: 'positional',
      arch: 'upper',
      side: 'patient_right',
      position: 6,
      spoken: 'شش بالا راست',
    });
  });

  it('narrows an explicit FDI tooth, which wins over positional fields', () => {
    const result = toVoiceIntent(
      wire({ teeth: [{ ...positionalTooth, fdi: '14', spoken: 'یک چهار' }] }),
    );
    expect(result.teeth[0]).toEqual({
      kind: 'explicit',
      fdi: '14',
      spoken: 'یک چهار',
    });
  });

  it('maps due.kind "none" to no deadline', () => {
    expect(toVoiceIntent(wire()).due).toBeNull();
  });

  it('narrows each due kind', () => {
    expect(
      toVoiceIntent(
        wire({
          due: {
            ...emptyDue,
            kind: 'weekday',
            weekday: 'thursday',
            which: 'next',
          },
        }),
      ).due,
    ).toEqual({ kind: 'weekday', weekday: 'thursday', which: 'next' });

    expect(
      toVoiceIntent(
        wire({ due: { ...emptyDue, kind: 'offset', unit: 'week', amount: 2 } }),
      ).due,
    ).toEqual({ kind: 'offset', unit: 'week', amount: 2 });

    expect(
      toVoiceIntent(
        wire({ due: { ...emptyDue, kind: 'jalali', jy: 1404, jm: 7, jd: 25 } }),
      ).due,
    ).toEqual({ kind: 'jalali', jy: 1404, jm: 7, jd: 25 });

    expect(
      toVoiceIntent(
        wire({
          due: { ...emptyDue, kind: 'gregorian', y: 2025, m: 10, d: 17 },
        }),
      ).due,
    ).toEqual({ kind: 'gregorian', y: 2025, m: 10, d: 17 });
  });

  it('ignores a non-FDI-shaped fdi and keeps the positional fields', () => {
    // A model emitting fdi:"6" alongside correct arch/side/position must still yield 16,
    // not lose the tooth to the explicit branch.
    const result = toVoiceIntent(
      wire({ teeth: [{ ...positionalTooth, fdi: '6' }] }),
    );
    expect(result.teeth[0]).toEqual({
      kind: 'positional',
      arch: 'upper',
      side: 'patient_right',
      position: 6,
      spoken: 'شش بالا راست',
    });
  });

  it('rejects impossible FDI shapes from the explicit branch', () => {
    for (const fdi of ['99', '0', '140', '9', 'ab']) {
      expect(
        toVoiceIntent(wire({ teeth: [{ ...positionalTooth, fdi }] })).teeth[0]
          .kind,
      ).toBe('positional');
    }
  });

  it('passes an unrecognised due kind through so it can be flagged', () => {
    // Collapsing it to null would make a misunderstood deadline indistinguishable from
    // no deadline at all, and the resolver's flagging path unreachable.
    const result = toVoiceIntent(
      wire({ due: { ...emptyDue, kind: 'lunar_month' as never } }),
    );
    expect(result.due).toEqual({ kind: 'lunar_month' });
  });

  it('reports no prosthesis when neither a default nor an override was given', () => {
    expect(toVoiceIntent(wire()).prosthesis).toBeNull();
  });

  it('builds a prosthesis intent from a default alone', () => {
    const result = toVoiceIntent(wire({ prosthesisDefaultType: 'pfm_crown' }));
    expect(result.prosthesis).toEqual({
      defaultType: 'pfm_crown',
      overrides: [],
    });
  });

  it('builds a prosthesis intent from overrides alone', () => {
    const result = toVoiceIntent(
      wire({
        prosthesisOverrides: [{ tooth: positionalTooth, type: 'pfm_crown' }],
      }),
    );
    expect(result.prosthesis?.defaultType).toBeNull();
    expect(result.prosthesis?.overrides).toHaveLength(1);
  });

  it('narrows connected spans', () => {
    const result = toVoiceIntent(
      wire({
        connectedSpans: [
          {
            from: { ...positionalTooth, fdi: '14' },
            to: { ...positionalTooth, fdi: '16' },
          },
        ],
      }),
    );
    expect(result.connectedSpans[0].from).toEqual({
      kind: 'explicit',
      fdi: '14',
      spoken: 'شش بالا راست',
    });
  });

  it('is total — a missing or malformed payload yields a resolvable shape, not a throw', () => {
    // Whatever survives here is reported as unresolved downstream rather than crashing.
    for (const bad of [undefined, null, {}, { teeth: 'nope', due: 5 }]) {
      expect(() => toVoiceIntent(bad as never)).not.toThrow();
      const result = toVoiceIntent(bad as never);
      expect(result.teeth).toEqual([]);
      expect(result.due).toBeNull();
      expect(result.labMatchExact).toBe(false);
    }
  });

  it('coerces a non-boolean labMatchExact to false', () => {
    expect(
      toVoiceIntent(wire({ labMatchExact: 'yes' as never })).labMatchExact,
    ).toBe(false);
  });
});
