import type { DdxDiagnosisGroupsView } from './ddx-view';
import type { InsightRequest, InsightTreatmentOut } from './insight-payload';
import {
  DisclosureScreenInput,
  ScreenBasicRow,
  buildRecordLinker,
  toDisclosureScreen,
  toScreenDate,
  withDisclosureScreen,
} from './insight-screen';

/**
 * 건강체 · 유병자 탭 결과 → ddx `disclosure_screen` (2026-10-13 「AI 인사이트와 원문 데이터가 차이가 없어야 한다」).
 * 합성 입력만 쓴다 — 탭 함수의 반환 모양(`getBasicList` · `getTreatment7DayGroups` · 30일 상병별 뷰)을 손으로 짓는다.
 */

const treat = (
  id: string,
  treatedOn: string,
  code: string,
  over: Partial<InsightTreatmentOut> = {},
): InsightTreatmentOut => ({
  treated_on: treatedOn,
  disease_code: code,
  disease_name: `합성 ${code}`,
  kind: '외래',
  department: null,
  facility_name: '합성의원',
  visit_days: 1,
  deductible_amt: null,
  insurance_type: '건강보험',
  ...over,
  id,
});

const row = (
  date: string | null,
  code: string,
  over: Partial<ScreenBasicRow['basic']> = {},
  detail: Partial<ScreenBasicRow['detail']> = {},
): ScreenBasicRow => ({
  basic: {
    asbTreatStartDate: date,
    asbHospitalName: '합성의원',
    asbTreatType: '외래',
    asbDiseaseCode: code,
    asbDiseaseName: `합성 ${code}`,
    asbVisitDays: 1,
    ...over,
  },
  detail: { asdOperation: null, examinationCount: 0, ...detail },
});

const dosing = (
  status: string,
  groups: DdxDiagnosisGroupsView['groups'] = [],
): DdxDiagnosisGroupsView => ({
  status,
  ingestRevision: 2,
  notice: null,
  groups,
  drugClasses: [],
});

const input = (
  over: Partial<DisclosureScreenInput> = {},
): DisclosureScreenInput => ({
  orderCreatedAt: '2026-10-01 10:20:30',
  recent3m: [],
  medicines: [],
  exam1y: [],
  care7d: [],
  dosing: dosing('ready'),
  adm5y: [],
  adm2y: [],
  major11: [],
  major6: [],
  ...over,
});

const checkOf = (
  screen: ReturnType<typeof toDisclosureScreen>,
  type: string,
  key: string,
) => screen.checks.find((c) => c.customer_type === type && c.key === key)!;

describe('toDisclosureScreen', () => {
  it('문항 열 개를 고정 순서로 내고 창은 주문일 기준이다', () => {
    const screen = toDisclosureScreen(input(), []);
    expect(screen.base_date).toBe('2026-10-01');
    expect(screen.checks.map((c) => `${c.customer_type}:${c.key}`)).toEqual([
      '건강체:3m',
      '건강체:exam1y',
      '건강체:care7d',
      '건강체:rx30d',
      '건강체:adm5y',
      '건강체:major11',
      '유병자:3m',
      '유병자:adm2y',
      '유병자:adm5y',
      '유병자:major6',
    ]);
    const win = (type: string, key: string) => {
      const c = checkOf(screen, type, key);
      return [c.window_from, c.window_to];
    };
    expect(win('건강체', '3m')).toEqual(['2026-07-01', '2026-10-01']);
    expect(win('건강체', 'exam1y')).toEqual(['2025-10-01', '2026-10-01']);
    expect(win('유병자', 'adm2y')).toEqual(['2024-10-01', '2026-10-01']);
    expect(win('건강체', 'rx30d')).toEqual(['2021-10-01', '2026-10-01']);
  });

  it('1년 내 검사 — 상병마다 탭의 검사 건수(COUNT(asd_examination))를 더한다', () => {
    const screen = toDisclosureScreen(
      input({
        exam1y: [
          row('2026-02-02', 'K0401', {}, { examinationCount: 2 }),
          row('2026-02-09', 'K0401', {}, { examinationCount: 3 }),
          row('2025-12-01', 'I109', {}, { examinationCount: 30 }),
        ],
      }),
      [],
    );
    const exam = checkOf(screen, '건강체', 'exam1y');
    expect(exam.groups.map((g) => [g.code, g.value, g.rows.length])).toEqual([
      ['K0401', 5, 2],
      ['I109', 30, 1],
    ]);
  });

  it('7일 이상 치료 — 탭과 같은 진료를 동일 상병으로 묶고 7일 이상만 싣는다(2026-10-14)', () => {
    //* 같은 중분류(M50-M54)의 M5456 4일 + 4일 = 8일은 서고, M5450 4일은 유사 병명이라 따로 세어 빠진다.
    const screen = toDisclosureScreen(
      input({
        care7d: [
          {
            middleCode: 'M50-M54',
            middleName: '기타 등병증',
            treatStartDate: '2026-01-02',
            hospitalName: '합성의원',
            diseaseCode: 'M5456',
            diseaseName: '(한방)요통, 요추부',
            visitDays: 4,
          },
          {
            middleCode: 'M50-M54',
            middleName: '기타 등병증',
            treatStartDate: '2026-02-02',
            hospitalName: '합성의원',
            diseaseCode: 'M5456',
            diseaseName: '(한방)요통, 요추부',
            visitDays: '4',
          },
          {
            middleCode: 'M50-M54',
            middleName: '기타 등병증',
            treatStartDate: '2026-03-02',
            hospitalName: '합성의원',
            diseaseCode: 'M5450',
            diseaseName: '(한방)요통, 다발 부위',
            visitDays: 4,
          },
        ],
      }),
      [treat('T-0001', '2026-01-02', 'M5456')],
    );
    const groups = checkOf(screen, '건강체', 'care7d').groups;
    expect(groups.map((g) => [g.code, g.name, g.value])).toEqual([
      ['M5456', '(한방)요통, 요추부', 8],
    ]);
    expect(groups[0].rows.map((r) => r.record_id)).toEqual(['T-0001', null]);
  });

  it('30일 이상 투약 — 상병별 탭 뷰의 투약일수 그대로이고, 판정이 없으면 pending', () => {
    const view = dosing('ready', [
      {
        dxCode: 'M1995',
        dxName: '상세불명의 관절증',
        isAdministrative: false,
        isUnknownDx: false,
        episodes: 1,
        startDate: '2025-03-04',
        endDate: '2025-03-04',
        medicationDays: 90,
        buckets: [
          {
            bucket: '주치료',
            drugs: [
              {
                prescriptions: [
                  {
                    treatmentStartDate: '2025-03-04',
                    provider: '합성의원',
                    dosePerAdministration: null,
                    administrationsPerDay: null,
                    daysSupplied: 90,
                  },
                  {
                    treatmentStartDate: '2025-03-04',
                    provider: '합성의원',
                    dosePerAdministration: null,
                    administrationsPerDay: null,
                    daysSupplied: 90,
                  },
                ],
              },
            ],
          },
        ],
      } as never,
    ]);
    const screen = toDisclosureScreen(input({ dosing: view }), [
      treat('T-0001', '2025-03-04', 'M1995'),
    ]);
    const rx = checkOf(screen, '건강체', 'rx30d');
    expect(rx.status).toBe('ready');
    expect(rx.groups).toEqual([
      {
        code: 'M1995',
        name: '상세불명의 관절증',
        label: null,
        value: 90,
        rows: [
          {
            record_id: 'T-0001',
            treated_on: '2025-03-04',
            facility_name: '합성의원',
            disease_code: 'M1995',
            kind: null,
            visit_days: null,
            surgery_name: null,
          },
        ],
      },
    ]);
    expect(
      checkOf(
        toDisclosureScreen(input({ dosing: dosing('none') }), []),
        '건강체',
        'rx30d',
      ).status,
    ).toBe('pending');
  });

  it('중대질병 — 카드마다 상세 행을 싣고 분류명은 asb_disease 다', () => {
    const screen = toDisclosureScreen(
      input({
        major11: [
          {
            code: 'I109',
            name: '고혈압',
            rows: [row('2025-01-02', 'I109', { asbDisease: '고혈압' })],
          },
        ],
      }),
      [],
    );
    const [g] = checkOf(screen, '건강체', 'major11').groups;
    expect([g.code, g.name, g.label, g.rows.length]).toEqual([
      'I109',
      '고혈압',
      '고혈압',
      1,
    ]);
    expect(checkOf(screen, '유병자', 'major6').groups).toEqual([]);
  });

  it('3개월 내 — 탭의 최신 조제이력 카드를 표지 붙은 묶음으로 싣는다', () => {
    const screen = toDisclosureScreen(
      input({
        recent3m: [row('2026-09-01', 'J209')],
        medicines: [
          {
            dispenseDate: '20260920',
            prescribingHospital: '합성의원',
            dispensingPharmacy: '합성약국',
          },
        ],
      }),
      [],
    );
    const groups = checkOf(screen, '건강체', '3m').groups;
    expect(
      groups.map((g) => [
        g.code,
        g.label,
        g.rows[0].treated_on,
        g.rows[0].facility_name,
      ]),
    ).toEqual([
      ['J209', null, '2026-09-01', '합성의원'],
      [null, '최신 조제이력', '2026-09-20', '합성약국'],
    ]);
    expect(checkOf(screen, '유병자', '3m').groups).toEqual(groups);
  });

  it('입원/수술 — 행마다 진료형태 · 수술명을 싣고 진료일 없는 행은 싣지 않는다', () => {
    const screen = toDisclosureScreen(
      input({
        adm5y: [
          row(
            '2025-01-02',
            'K409',
            { asbTreatType: '입원', asbVisitDays: 3 },
            { asdOperation: '서혜부탈장수술' },
          ),
          row(null, 'K409'),
        ],
      }),
      [],
    );
    const [g] = checkOf(screen, '건강체', 'adm5y').groups;
    expect(g.rows).toEqual([
      expect.objectContaining({
        kind: '입원',
        visit_days: 3,
        surgery_name: '서혜부탈장수술',
      }),
    ]);
  });
});

describe('buildRecordLinker', () => {
  it('같은 날 · 같은 병원 · 같은 상병 입원 둘(0일 · 15일)을 각각 잇는다(2026-10-14 고객사)', () => {
    const linker = buildRecordLinker([
      treat('T-0001', '2025-02-12', 'M5456', { kind: '입원', visit_days: 0 }),
      treat('T-0002', '2025-02-12', 'M5456', { kind: '입원', visit_days: 15 }),
    ]);
    const link = linker();
    expect(link('2025-02-12', '합성의원', 'M5456', '입원', 15)).toBe('T-0002');
    expect(link('2025-02-12', '합성의원', 'M5456', '입원', 0)).toBe('T-0001');
    //* 내원일수를 모르면 아직 안 이은 진료부터 — 둘째 행이 첫 진료로 겹치지 않는다.
    const blind = linker();
    expect([
      blind('2025-02-12', '합성의원', 'M5456', '입원'),
      blind('2025-02-12', '합성의원', 'M5456', '입원'),
    ]).toEqual(['T-0001', 'T-0002']);
    //* 탭 행이 진료보다 많으면(같은 진료가 두 행) 이은 진료를 다시 쓴다 — 「잇지 못함」 이 아니다.
    const dup = buildRecordLinker([treat('T-0001', '2025-02-12', 'M5456')])();
    expect([
      dup('2025-02-12', '합성의원', 'M5456', '외래', 1),
      dup('2025-02-12', '합성의원', 'M5456', '외래', 1),
    ]).toEqual(['T-0001', 'T-0001']);
  });

  it('입원/수술 문항 — 두 입원 행이 두 진료로 이어진다', () => {
    const screen = toDisclosureScreen(
      input({
        adm5y: [
          row('2025-02-12', 'M5456', { asbTreatType: '입원', asbVisitDays: 0 }),
          row('2025-02-12', 'M5456', {
            asbTreatType: '입원',
            asbVisitDays: 15,
          }),
        ],
      }),
      [
        treat('T-0001', '2025-02-12', 'M5456', { kind: '입원', visit_days: 0 }),
        treat('T-0002', '2025-02-12', 'M5456', {
          kind: '입원',
          visit_days: 15,
        }),
      ],
    );
    const [g] = checkOf(screen, '건강체', 'adm5y').groups;
    expect(g.rows.map((r) => [r.record_id, r.visit_days])).toEqual([
      ['T-0001', 0],
      ['T-0002', 15],
    ]);
  });

  it('(진료일, 기관, 상병) → 진료형태로 좁힘 → 기관이 다르면 (진료일, 상병) 한 건만', () => {
    const link = buildRecordLinker([
      treat('T-0001', '2025-01-02', 'K409', { kind: '외래' }),
      treat('T-0002', '2025-01-02', 'K409', { kind: '입원' }),
      treat('T-0003', '2025-02-02', 'J209', { facility_name: '다른의원' }),
      treat('T-0004', '2025-03-02', 'J209', { facility_name: '가의원' }),
      treat('T-0005', '2025-03-02', 'J209', { facility_name: '나의원' }),
      treat('T-0006', '2025-04-02', 'S810', { insurance_type: '자동차보험' }),
    ])();
    expect(link('2025-01-02', '합성의원', 'K409', '입원')).toBe('T-0002');
    expect(link('2025-01-02', '합성의원', 'K409', null)).toBe('T-0001');
    expect(link('2025-02-02', '합성의원', 'J209', null)).toBe('T-0003');
    expect(link('2025-03-02', '합성의원', 'J209', null)).toBeNull(); //* 모호 — 덜 가리킨다
    expect(link('2025-04-02', '합성의원', 'S810', null)).toBeNull(); //* 자동차보험은 후보 아님
    expect(link('2025-01-02', '합성의원', null, null)).toBeNull();
  });

  it('toScreenDate — 없는 날짜는 null 이다(ddx 422 방지)', () => {
    expect([
      toScreenDate('2025-02-30'),
      toScreenDate('20251301'),
      toScreenDate('2024-02-29'),
    ]).toEqual([null, null, '2024-02-29']);
  });

  it('toScreenDate — YYYY-MM-DD · YYYYMMDD 만 받는다', () => {
    expect([
      toScreenDate('2025-03-04 00:00:00'),
      toScreenDate('20250304'),
      toScreenDate('2025.03.04'),
      toScreenDate(null),
    ]).toEqual(['2025-03-04', '2025-03-04', null, null]);
  });
});

describe('withDisclosureScreen', () => {
  const base = (treatments: InsightTreatmentOut[]): InsightRequest => ({
    as_of: '2026-10-13',
    treatments,
    notices: [],
  });

  it('마지막 키로 붙고, id 가 없는 요청에는 T-0001 부터 매긴다', () => {
    const plain = treat('x', '2026-09-01', 'J209');
    delete (plain as Partial<InsightTreatmentOut>).id;
    const out = withDisclosureScreen(
      base([plain]),
      input({ recent3m: [row('2026-09-01', 'J209')] }),
    );
    expect(Object.keys(out)).toEqual([
      'as_of',
      'treatments',
      'notices',
      'disclosure_screen',
    ]);
    expect(out.treatments[0].id).toBe('T-0001');
    expect(Object.keys(out.treatments[0]).pop()).toBe('id');
    expect(
      checkOf(out.disclosure_screen!, '건강체', '3m').groups[0].rows[0]
        .record_id,
    ).toBe('T-0001');
  });

  it('주문일을 못 읽으면 싣지 않는다(ddx 422 방지)', () => {
    const request = base([treat('T-0001', '2026-09-01', 'J209')]);
    expect(
      withDisclosureScreen(request, input({ orderCreatedAt: 'not-a-date' })),
    ).toBe(request);
  });

  it('측정값이 정수가 아니거나 음수면 null 이다(ddx 422 방지)', () => {
    const screen = toDisclosureScreen(
      input({
        care7d: [
          {
            middleCode: 'M50-M54',
            middleName: '기타 등병증',
            treatStartDate: '2026-01-02',
            hospitalName: '합성의원',
            diseaseCode: 'M5456',
            visitDays: 7.5,
          },
        ],
        dosing: dosing('ready', [
          {
            dxCode: 'M1995',
            dxName: 'x',
            isAdministrative: false,
            isUnknownDx: false,
            episodes: 1,
            startDate: null,
            endDate: null,
            medicationDays: -1,
            buckets: [],
          } as never,
        ]),
      }),
      [],
    );
    expect(checkOf(screen, '건강체', 'care7d').groups[0].value).toBeNull();
    expect(checkOf(screen, '건강체', 'rx30d').groups[0].value).toBeNull();
  });

  it('id 가 있으면 그대로 쓴다', () => {
    const out = withDisclosureScreen(
      base([treat('T-0009', '2026-09-01', 'J209')]),
      input(),
    );
    expect(out.treatments[0].id).toBe('T-0009');
  });
});
