import { CatalogEntityKind } from '@prisma/client';
import {
  PROSTHESIS_TYPES,
  buildProsthesisStepCodes,
} from '../../../prisma/catalog-seed-data';
import { generateLabCaseTasks } from './lab-case-task.generator';

function buildMockTx(options: {
  existingCount?: number;
  toothProsthesisRows: Array<{
    treatmentDetailId?: string | null;
    lineId?: string | null;
    sourceKey?: string;
    tooth: string;
    prosthesisTypeCode: string;
    selectionGroupId?: string;
    treatmentType?: string;
  }>;
  prosthesisTypes: Array<{
    code: string;
    steps: Array<{ stepOrder: number; workflowStepCode: string }>;
  }>;
  stepLabels?: Record<string, string>;
}) {
  const created: unknown[] = [];

  const tx = {
    labCaseTask: {
      count: jest.fn().mockResolvedValue(options.existingCount ?? 0),
      createMany: jest.fn().mockImplementation(({ data }) => {
        created.push(...data);
        return { count: data.length };
      }),
    },
    labCaseToothProsthesis: {
      findMany: jest.fn().mockResolvedValue(
        options.toothProsthesisRows.map((row) => ({
          treatmentDetailId: row.treatmentDetailId ?? null,
          lineId: row.lineId ?? null,
          sourceKey: row.sourceKey ?? row.treatmentDetailId ?? row.lineId ?? '',
          treatmentType: row.treatmentType ?? 'prosthesis',
          tooth: row.tooth,
          prosthesisTypeCode: row.prosthesisTypeCode,
          selectionGroupId: row.selectionGroupId ?? '',
          detail: row.treatmentDetailId
            ? {
                id: row.treatmentDetailId,
                treatmentType: row.treatmentType ?? 'prosthesis',
                toothSelectionGroups: null,
              }
            : null,
          line: row.lineId
            ? {
                id: row.lineId,
                treatmentType: row.treatmentType ?? 'prosthesis',
                toothSelectionGroups: null,
              }
            : null,
        })),
      ),
    },
    prosthesisType: {
      findMany: jest.fn().mockResolvedValue(
        options.prosthesisTypes.map((type) => ({
          code: type.code,
          isActive: true,
          steps: type.steps.map((step) => ({
            stepOrder: step.stepOrder,
            labWorkflowStep: { code: step.workflowStepCode },
          })),
        })),
      ),
    },
    catalogTranslation: {
      findMany: jest.fn().mockImplementation(({ where }) => {
        const codes = where.entityCode?.in ?? [];
        return codes.map((code: string) => ({
          entityCode: code,
          locale: 'en',
          label: options.stepLabels?.[code] ?? code,
          entityKind: CatalogEntityKind.LAB_WORKFLOW_STEP,
        }));
      }),
    },
  };

  return { tx, created };
}

function stepsFromSeed(code: string) {
  const seed = PROSTHESIS_TYPES.find((type) => type.code === code);
  if (!seed) {
    throw new Error(`Unknown prosthesis code: ${code}`);
  }
  return buildProsthesisStepCodes(seed).map((workflowStepCode, index) => ({
    stepOrder: index + 1,
    workflowStepCode,
  }));
}

describe('generateLabCaseTasks', () => {
  it('creates tasks for pfm_crown with universal and type-specific steps', async () => {
    const pfmSteps = stepsFromSeed('pfm_crown');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '14',
          prosthesisTypeCode: 'pfm_crown',
        },
      ],
      prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }],
      stepLabels: { intraoral_scan: 'Intraoral Scan', packing: 'Packing' },
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-1', 'en');

    expect(count).toBe(pfmSteps.length);
    expect(created).toHaveLength(pfmSteps.length);
    expect(created[0]).toMatchObject({
      teeth: ['14'],
      prosthesisTypeCode: 'pfm_crown',
      workflowStepCode: 'intraoral_scan',
      stepLabel: 'Intraoral Scan',
      status: 'IN_PROGRESS',
    });
    const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
      (row) => row.workflowStepCode,
    );
    expect(stepCodes).toEqual(pfmSteps.map((step) => step.workflowStepCode));
    expect(stepCodes).toContain('packing');
    expect(stepCodes).toContain('shipping');
    expect(stepCodes).toContain('milling_wet');
  });

  it('keeps one task set per tooth even when they share a prosthesis', async () => {
    const pfmSteps = stepsFromSeed('pfm_crown');
    const zirconiaSteps = stepsFromSeed('monolithic_zirconia');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        { treatmentDetailId: 'detail-1', tooth: '15', prosthesisTypeCode: 'pfm_crown' },
        { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'pfm_crown' },
        { treatmentDetailId: 'detail-1', tooth: '16', prosthesisTypeCode: 'monolithic_zirconia' },
      ],
      prosthesisTypes: [
        { code: 'pfm_crown', steps: pfmSteps },
        { code: 'monolithic_zirconia', steps: zirconiaSteps },
      ],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-group', 'en');

    expect(count).toBe(pfmSteps.length * 2 + zirconiaSteps.length);
    const rows = created as Array<{ teeth: string[]; prosthesisTypeCode: string }>;
    const pfm14 = rows.filter(
      (r) => r.prosthesisTypeCode === 'pfm_crown' && r.teeth.join() === '14',
    );
    const pfm15 = rows.filter(
      (r) => r.prosthesisTypeCode === 'pfm_crown' && r.teeth.join() === '15',
    );
    const zirconiaRows = rows.filter((r) => r.prosthesisTypeCode === 'monolithic_zirconia');

    expect(pfm14).toHaveLength(pfmSteps.length);
    expect(pfm15).toHaveLength(pfmSteps.length);
    expect(zirconiaRows).toHaveLength(zirconiaSteps.length);
    expect(zirconiaRows.every((r) => JSON.stringify(r.teeth) === JSON.stringify(['16']))).toBe(true);
    const uniqueKeys = new Set(
      rows.map((r) => `${r.teeth.join()}:${r.prosthesisTypeCode}`),
    );
    expect(uniqueKeys.size).toBe(3);
  });

  it('omits packing and shipping for smile_design', async () => {
    const smileSteps = stepsFromSeed('smile_design');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '11',
          prosthesisTypeCode: 'smile_design',
        },
      ],
      prosthesisTypes: [{ code: 'smile_design', steps: smileSteps }],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-2', 'en');

    expect(count).toBe(smileSteps.length);
    const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
      (row) => row.workflowStepCode,
    );
    expect(stepCodes).toEqual(['intraoral_scan', 'design']);
    expect(stepCodes).not.toContain('packing');
    expect(stepCodes).not.toContain('shipping');
  });

  it('merges connected teeth that share a prosthesis type into one task set', async () => {
    const pfmSteps = stepsFromSeed('pfm_crown');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '14',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'connected-1',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '15',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'connected-1',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '21',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'single-21',
        },
      ],
      prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-merge', 'en');
    expect(count).toBe(pfmSteps.length * 2);
    const rows = created as Array<{ teeth: string[]; prosthesisTypeCode: string }>;
    const teethSets = new Set(rows.map((r) => r.teeth.join(',')));
    expect(teethSets).toEqual(new Set(['14,15', '21']));
  });

  it('keeps an extra type on one connected tooth as its own task set', async () => {
    const pfmSteps = stepsFromSeed('pfm_crown');
    const postSteps = stepsFromSeed('fiber_post_core');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '14',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'connected-1',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '14',
          prosthesisTypeCode: 'fiber_post_core',
          selectionGroupId: 'connected-1',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '15',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'connected-1',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '16',
          prosthesisTypeCode: 'pfm_crown',
          selectionGroupId: 'connected-1',
        },
      ],
      prosthesisTypes: [
        { code: 'pfm_crown', steps: pfmSteps },
        { code: 'fiber_post_core', steps: postSteps },
      ],
    });

    await generateLabCaseTasks(tx as never, 'lab-case-bridge-addon', 'en');
    const rows = created as Array<{
      teeth: string[];
      prosthesisTypeCode: string;
    }>;
    const byType = new Map<string, string>();
    for (const row of rows) {
      byType.set(row.prosthesisTypeCode, row.teeth.join(','));
    }
    expect(byType.get('pfm_crown')).toBe('14,15,16');
    expect(byType.get('fiber_post_core')).toBe('14');
  });

  it('skips generation when tasks already exist', async () => {
    const { tx } = buildMockTx({
      existingCount: 3,
      toothProsthesisRows: [],
      prosthesisTypes: [],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-3', 'en');

    expect(count).toBe(0);
    expect(tx.labCaseTask.createMany).not.toHaveBeenCalled();
  });

  it('creates tasks from lab-origin lines using sourceKey and lineId', async () => {
    const pfmSteps = stepsFromSeed('pfm_crown');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          lineId: 'line-1',
          sourceKey: 'line-1',
          tooth: '11',
          prosthesisTypeCode: 'pfm_crown',
        },
        {
          lineId: 'line-1',
          sourceKey: 'line-1',
          tooth: '21',
          prosthesisTypeCode: 'pfm_crown',
        },
      ],
      prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-internal-1', 'en');
    expect(count).toBe(pfmSteps.length * 2);
    const rows = created as Array<{ teeth: string[]; lineId: string }>;
    const teethSets = new Set(rows.map((r) => r.teeth.join(',')));
    expect(teethSets).toEqual(new Set(['11', '21']));
    expect(created[0]).toMatchObject({
      lineId: 'line-1',
      sourceKey: 'line-1',
      treatmentDetailId: null,
    });
  });

  it('unions stacked types on the same tooth and de-dupes scan/pack/ship', async () => {
    const tiSteps = stepsFromSeed('ti_base_abutment');
    const zrSteps = stepsFromSeed('monolithic_zirconia');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '46',
          prosthesisTypeCode: 'ti_base_abutment',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '46',
          prosthesisTypeCode: 'monolithic_zirconia',
        },
      ],
      prosthesisTypes: [
        { code: 'ti_base_abutment', steps: tiSteps },
        { code: 'monolithic_zirconia', steps: zrSteps },
      ],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-stack', 'en');
    const rows = created as Array<{
      workflowStepCode: string;
      prosthesisTypeCode: string;
      teeth: string[];
    }>;
    expect(count).toBe(rows.length);
    expect(rows.every((r) => r.teeth.join() === '46')).toBe(true);
    expect(rows[0].prosthesisTypeCode).toBe('monolithic_zirconia+ti_base_abutment');
    const stepCodes = rows.map((r) => r.workflowStepCode);
    expect(stepCodes.filter((c) => c === 'intraoral_scan')).toHaveLength(1);
    expect(stepCodes.filter((c) => c === 'packing')).toHaveLength(1);
    expect(stepCodes.filter((c) => c === 'shipping')).toHaveLength(1);
    expect(stepCodes[0]).toBe('intraoral_scan');
    expect(stepCodes[stepCodes.length - 1]).toBe('shipping');
    expect(stepCodes).toContain('milling_dry');
    expect(stepCodes).toContain('sinter');
    expect(stepCodes).toContain('polish_prep');
    expect(stepCodes).toContain('model_create');
    expect(stepCodes).toContain('design');
    expect(stepCodes.indexOf('model_create')).toBeGreaterThan(stepCodes.indexOf('design'));
  });

  it('omits design for prefab abutments and inserts choosing_abutment before model_create', async () => {
    const prefabSteps = stepsFromSeed('prefabricated_abutment');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '36',
          prosthesisTypeCode: 'prefabricated_abutment',
        },
      ],
      prosthesisTypes: [{ code: 'prefabricated_abutment', steps: prefabSteps }],
    });

    await generateLabCaseTasks(tx as never, 'lab-case-prefab', 'en');
    const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
      (row) => row.workflowStepCode,
    );
    expect(stepCodes).toEqual([
      'intraoral_scan',
      'choosing_abutment',
      'model_create',
      'polish_prep',
      'packing',
      'shipping',
    ]);
    expect(stepCodes).not.toContain('design');
  });

  it('pins choosing_abutment before design when stacking prefab with a crown', async () => {
    const prefabSteps = stepsFromSeed('prefabricated_abutment');
    const zrSteps = stepsFromSeed('monolithic_zirconia');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        {
          treatmentDetailId: 'detail-1',
          tooth: '46',
          prosthesisTypeCode: 'prefabricated_abutment',
        },
        {
          treatmentDetailId: 'detail-1',
          tooth: '46',
          prosthesisTypeCode: 'monolithic_zirconia',
        },
      ],
      prosthesisTypes: [
        { code: 'prefabricated_abutment', steps: prefabSteps },
        { code: 'monolithic_zirconia', steps: zrSteps },
      ],
    });

    await generateLabCaseTasks(tx as never, 'lab-case-union-pin', 'en');
    const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
      (row) => row.workflowStepCode,
    );
    expect(stepCodes[0]).toBe('intraoral_scan');
    expect(stepCodes.indexOf('choosing_abutment')).toBeLessThan(stepCodes.indexOf('design'));
    expect(stepCodes.indexOf('design')).toBeLessThan(stepCodes.indexOf('model_create'));
    expect(stepCodes.filter((c) => c === 'intraoral_scan')).toHaveLength(1);
  });

  it('merges partial_denture rows on the same sourceKey into one task set', async () => {
    const partialSteps = stepsFromSeed('partial_denture');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '15', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '16', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '17', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '18', prosthesisTypeCode: 'partial_denture' },
      ],
      prosthesisTypes: [{ code: 'partial_denture', steps: partialSteps }],
    });

    const count = await generateLabCaseTasks(tx as never, 'lab-case-partial', 'en');
    expect(count).toBe(partialSteps.length);
    const rows = created as Array<{
      teeth: string[];
      tooth: string;
      prosthesisTypeCode: string;
    }>;
    expect(rows.every((r) => r.prosthesisTypeCode === 'partial_denture')).toBe(true);
    expect(rows.every((r) => r.teeth.join(',') === '14,15,16,17,18')).toBe(true);
    expect(new Set(rows.map((r) => r.tooth)).size).toBe(1);
  });

  it('keeps crowns on abutment teeth as separate per-tooth jobs beside a merged partial', async () => {
    const partialSteps = stepsFromSeed('partial_denture');
    const pfmSteps = stepsFromSeed('pfm_crown');
    const { tx, created } = buildMockTx({
      toothProsthesisRows: [
        { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '15', prosthesisTypeCode: 'partial_denture' },
        { treatmentDetailId: 'detail-1', tooth: '14', prosthesisTypeCode: 'pfm_crown' },
      ],
      prosthesisTypes: [
        { code: 'partial_denture', steps: partialSteps },
        { code: 'pfm_crown', steps: pfmSteps },
      ],
    });

    await generateLabCaseTasks(tx as never, 'lab-case-partial-crown', 'en');
    const rows = created as Array<{
      teeth: string[];
      prosthesisTypeCode: string;
    }>;
    const partialRows = rows.filter((r) => r.prosthesisTypeCode === 'partial_denture');
    const crownRows = rows.filter((r) => r.prosthesisTypeCode === 'pfm_crown');
    expect(partialRows).toHaveLength(partialSteps.length);
    expect(partialRows.every((r) => r.teeth.join(',') === '14,15')).toBe(true);
    expect(crownRows).toHaveLength(pfmSteps.length);
    expect(crownRows.every((r) => r.teeth.join(',') === '14')).toBe(true);
  });
});
