import { describe, expect, it } from 'vitest';
import type { ImagehubFile } from '@ump/shared';

import {
  buildWorkspaceModel,
  caseStatus,
  datasetJsonPreview,
  dominantEnding,
  formatBytes,
  inferEnding,
} from './workspaceModel';

function f(p: Partial<ImagehubFile> & { logicalPath: string }): ImagehubFile {
  return {
    id: p.id ?? p.logicalPath,
    logicalPath: p.logicalPath,
    folderPath: p.folderPath ?? '',
    sha256: 'sha',
    size: p.size ?? 1000,
    mediaType: 'application/octet-stream',
    imagingMeta: {},
    fileKind: p.fileKind ?? 'image',
    parentFileId: p.parentFileId ?? null,
    organLabel: p.organLabel ?? '',
    uploadedAt: null,
    downloadUrl: null,
  };
}

describe('inferEnding / dominantEnding', () => {
  it('longest-matches .nii.gz over .gz', () => {
    expect(inferEnding('case_001_0000.nii.gz')).toBe('.nii.gz');
    expect(inferEnding('img.png')).toBe('.png');
    expect(inferEnding('notes.txt')).toBeNull();
  });

  it('picks the dominant ending, defaulting to .nii.gz', () => {
    expect(dominantEnding([f({ logicalPath: 'a.nii.gz' }), f({ logicalPath: 'b.nii.gz' }), f({ logicalPath: 'c.png' })])).toBe('.nii.gz');
    expect(dominantEnding([])).toBe('.nii.gz');
  });
});

describe('buildWorkspaceModel — grouping', () => {
  it('groups multi-channel nnU-Net files into one case with both channels inferred', () => {
    const m = buildWorkspaceModel('Prostate', [
      f({ id: 'a', logicalPath: 'case_001_0000.nii.gz' }),
      f({ id: 'b', logicalPath: 'case_001_0001.nii.gz' }),
    ]);
    expect(m.cases).toHaveLength(1);
    expect(m.cases[0].caseId).toBe('case_001');
    expect(m.meta.channels.map((c) => c.index)).toEqual([0, 1]);
    // both channels present, no label -> warn (training readiness), not error
    expect(caseStatus(m.cases[0], m.meta)).toBe('warn');
    expect(m.organized).toBe(true);
  });

  it('treats a bare file as a single channel-0 image case', () => {
    const m = buildWorkspaceModel('DS', [f({ logicalPath: 'patient1.nii.gz' })]);
    expect(m.cases).toHaveLength(1);
    expect(m.cases[0].caseId).toBe('patient1');
    expect(m.cases[0].channelFiles[0]).toBeDefined();
  });

  it('attaches a segmentation mask as the label of its parent image case', () => {
    const m = buildWorkspaceModel('DS', [
      f({ id: 'img', logicalPath: 'case_001_0000.nii.gz' }),
      f({ id: 'seg', logicalPath: 'liver.nii.gz', fileKind: 'segmentation', parentFileId: 'img', organLabel: 'liver' }),
    ]);
    expect(m.cases).toHaveLength(1);
    expect(m.cases[0].labelFile).toBeDefined();
    // channel 0 present + label present -> valid
    expect(caseStatus(m.cases[0], m.meta)).toBe('valid');
  });

  it('flags a case missing one of the dataset channels as error', () => {
    const m = buildWorkspaceModel('DS', [
      f({ id: 'a', logicalPath: 'case_001_0000.nii.gz' }),
      f({ id: 'b', logicalPath: 'case_001_0001.nii.gz' }),
      f({ id: 'c', logicalPath: 'case_002_0000.nii.gz' }), // missing channel 1
    ]);
    const c2 = m.cases.find((c) => c.caseId === 'case_002')!;
    expect(caseStatus(c2, m.meta)).toBe('error');
  });

  it('recognizes a labelsTr/ file as the case label even when uploaded as fileKind image', () => {
    const m = buildWorkspaceModel('DS', [
      f({ id: 'img', logicalPath: 'KIT23_00000_0000.nii.gz', folderPath: 'imagesTr' }),
      f({ id: 'lbl', logicalPath: 'KIT23_00000.nii.gz', folderPath: 'labelsTr' }),
    ]);
    expect(m.cases).toHaveLength(1);
    const c = m.cases[0];
    expect(c.caseId).toBe('KIT23_00000');
    expect(c.labelFile?.id).toBe('lbl');
    expect(c.channelFiles[0]?.id).toBe('img');
    expect(caseStatus(c, m.meta)).toBe('valid');
  });
});

describe('buildWorkspaceModel — readiness report', () => {
  it('keeps per-case/file readiness issues and drops descriptor-level noise', () => {
    const m = buildWorkspaceModel('Has Spaces', [f({ logicalPath: 'case_001_0000.nii.gz' })]);
    // dataset.name (spaces) / dataset.id (reserved) noise must be filtered out
    expect(m.report.issues.some((i) => i.code.startsWith('dataset.'))).toBe(false);
    expect(m.report.issues.some((i) => i.code.startsWith('channels.'))).toBe(false);
    // a training case without a label is a kept readiness issue
    expect(m.report.issues.some((i) => i.code === 'case.missingLabel')).toBe(true);
  });

  it('returns an empty, unorganized model for a dataset with no files', () => {
    const m = buildWorkspaceModel('', []);
    expect(m.cases).toHaveLength(0);
    expect(m.organized).toBe(false);
    expect(m.report.ok).toBe(true);
  });
});

describe('helpers', () => {
  it('formatBytes', () => {
    expect(formatBytes(500)).toBe('500 B');
    expect(formatBytes(1024)).toBe('1.0 KB');
    expect(formatBytes(-1)).toBe('—');
  });

  it('datasetJsonPreview has the nnU-Net descriptor shape', () => {
    const m = buildWorkspaceModel('DS', [
      f({ id: 'a', logicalPath: 'case_001_0000.nii.gz' }),
      f({ id: 's', logicalPath: 'm.nii.gz', fileKind: 'segmentation', parentFileId: 'a' }),
    ]);
    const json = datasetJsonPreview(m) as Record<string, unknown>;
    expect(json).toHaveProperty('channel_names');
    expect(json).toHaveProperty('labels');
    expect(json).toHaveProperty('file_ending', '.nii.gz');
    expect(json.numTraining).toBe(1);
  });
});
