import { describe, expect, it } from 'vitest';

import {
  SourceReattachmentError,
  bestReattachmentCandidate,
  confirmSourceReattachment,
  rankReattachmentCandidates,
} from '../../../src/project/source-reattachment';
import type { SourceHash } from '../../../src/project/project.types';
import { UPDATED_AT, mediaAsset, populatedProject } from './project-fixture';

const HASH_A: SourceHash = {
  algorithm: 'sha-256',
  value: 'a'.repeat(64),
  scope: 'full',
};
const HASH_B: SourceHash = {
  algorithm: 'sha-256',
  value: 'b'.repeat(64),
  scope: 'full',
};

describe('source reattachment matching', () => {
  it('ranks size, timestamp, filename, MIME type and optional hash evidence', () => {
    const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
    const candidates = [
      {
        name: 'renamed.mp4',
        size: asset.size,
        lastModified: asset.lastModified,
        mimeType: asset.mimeType,
      },
      {
        name: asset.originalName,
        size: asset.size,
        lastModified: asset.lastModified,
        mimeType: asset.mimeType,
        hash: HASH_A,
      },
      {
        name: asset.originalName,
        size: asset.size,
        lastModified: asset.lastModified,
        mimeType: asset.mimeType,
        hash: HASH_B,
      },
    ];
    const matches = rankReattachmentCandidates(asset, candidates);

    expect(matches.map((match) => match.confidence)).toEqual([
      'exact',
      'likely',
      'none',
    ]);
    expect(matches[0]).toMatchObject({
      score: 190,
      requiresExplicitConfirmation: true,
    });
    expect(
      matches[0]?.evidence.find((evidence) => evidence.field === 'hash')
    ).toMatchObject({ matches: true });
  });

  it('does not suggest ambiguous ties', () => {
    const asset = mediaAsset({ sourceStatus: 'missing' });
    const candidates = [
      {
        name: 'a.mp4',
        size: asset.size,
        lastModified: asset.lastModified,
        mimeType: asset.mimeType,
      },
      {
        name: 'b.mp4',
        size: asset.size,
        lastModified: asset.lastModified,
        mimeType: asset.mimeType,
      },
    ];
    expect(bestReattachmentCandidate(asset, candidates)).toBeUndefined();
  });

  it('allows filesystem timestamp tolerance but treats size/hash mismatch as unsafe', () => {
    const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
    expect(
      rankReattachmentCandidates(asset, [
        {
          name: asset.originalName,
          size: asset.size,
          lastModified: asset.lastModified + 1_500,
          mimeType: asset.mimeType,
        },
      ])[0]?.confidence
    ).toBe('likely');
    expect(
      rankReattachmentCandidates(asset, [
        {
          name: asset.originalName,
          size: asset.size + 1,
          lastModified: asset.lastModified,
          mimeType: asset.mimeType,
        },
      ])[0]?.confidence
    ).toBe('none');
  });

  it('does not treat incomparable partial and full hashes as a mismatch', () => {
    const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
    const partial: SourceHash = {
      algorithm: 'sha-256',
      value: 'b'.repeat(64),
      scope: 'partial',
      bytesHashed: 1_024,
    };
    expect(
      rankReattachmentCandidates(asset, [
        {
          name: asset.originalName,
          size: asset.size,
          lastModified: asset.lastModified,
          mimeType: asset.mimeType,
          hash: partial,
        },
      ])[0]?.confidence
    ).toBe('likely');
  });

  it('rejects malformed file descriptors before scoring', () => {
    expect(() =>
      rankReattachmentCandidates(mediaAsset(), [
        {
          name: '',
          size: -1,
          lastModified: Number.NaN,
        },
      ])
    ).toThrow(SourceReattachmentError);
  });

  it('only marks a source attached after an explicit confirmation call', () => {
    const project = {
      ...populatedProject(),
      assets: [mediaAsset({ sourceStatus: 'missing' })],
    };
    const candidate = {
      name: 'renamed.mp4',
      size: 1_024,
      lastModified: 1_700_000_000_000,
      mimeType: 'video/mp4',
    };
    const result = confirmSourceReattachment(
      project,
      'asset-1',
      candidate,
      UPDATED_AT
    );

    expect(project.assets[0]?.sourceStatus).toBe('missing');
    expect(result.assets[0]?.sourceStatus).toBe('attached');
    expect(() =>
      confirmSourceReattachment(
        project,
        'asset-1',
        { ...candidate, size: 1 },
        UPDATED_AT
      )
    ).toThrow(SourceReattachmentError);
  });
});
