import { projectFilePath } from '@learnitive/project-storage';
import type { ProjectSemanticSnapshot, VisualBlock } from '@learnitive/project-semantic-index';
import { describe, expect, it } from 'vitest';
import {
  buildProjectMeaningChunks,
  createProjectMeaningReuseLookup,
  cosineSimilarity,
  EMBEDDING_MODEL_ID,
  EMBEDDING_MODEL_VERSION,
  normalizeVector,
  PROJECT_MEANING_SCHEMA_VERSION,
  reusableProjectMeaningVectorIndex,
  type StoredProjectMeaningChunk,
  type StoredProjectMeaningIndex,
} from '../index';

function chunk(id: string, contentHash: string, from: number): StoredProjectMeaningChunk {
  return {
    id,
    contentHash,
    file: projectFilePath('main.tex'),
    range: { from, to: from + 10 },
    line: 1,
    kind: 'prose',
    text: contentHash,
  };
}

describe('project meaning vectors', () => {
  it('normalizes and ranks cosine-compatible vectors', () => {
    const normalized = normalizeVector(new Float32Array([3, 4]));
    expect(normalized[0]).toBeCloseTo(0.6);
    expect(normalized[1]).toBeCloseTo(0.8);
    expect(cosineSimilarity(normalized, normalized)).toBeCloseTo(1);
  });


  it('builds scientific-aware passages from the existing deterministic projection', () => {
    const blocks = [
      { id: 'h', kind: 'heading', file: 'main.tex', range: { from: 0, to: 20 }, bodyRange: { from: 9, to: 19 }, text: 'Methods', level: 2 },
      { id: 'p1', kind: 'paragraph', file: 'main.tex', range: { from: 21, to: 80 }, bodyRange: { from: 21, to: 80 }, text: 'We evaluate robustness under distribution shift.' },
      { id: 'eq', kind: 'rich-paragraph', file: 'main.tex', range: { from: 81, to: 140 }, text: 'The loss is defined by an equation.', inlines: [{ id: 'm', kind: 'math', range: { from: 100, to: 110 }, bodyRange: { from: 101, to: 109 }, text: 'L(x)', mathLiveValue: 'L(x)', mathKind: 'inline', delimiter: '$', editable: true }] },
      { id: 'fig', kind: 'figure', file: 'main.tex', range: { from: 141, to: 200 }, text: 'figure source', caption: 'Robustness across datasets' },
      { id: 'tab', kind: 'table', file: 'main.tex', range: { from: 201, to: 280 }, text: 'table source', caption: 'Dataset results', editable: true, cells: [{ id: 'c', row: 0, column: 0, range: { from: 220, to: 225 }, bodyRange: { from: 220, to: 225 }, text: '0.91' }] },
      { id: 'thm', kind: 'theorem', file: 'main.tex', range: { from: 281, to: 350 }, bodyRange: { from: 300, to: 340 }, text: 'For every bounded perturbation the estimator is stable.', environment: 'theorem', editable: true },
    ] as unknown as VisualBlock[];
    const snapshot = {
      rootFile: 'main.tex',
      files: new Map([['main.tex', { file: 'main.tex', sourceLength: 350, nodes: [], visualBlocks: blocks }]]),
      outline: [], labels: [], references: [], citations: [], packages: [], assets: [], inputs: [], environments: [],
      bibliographyEntries: [], bibliographyFiles: [], diagnostics: [],
    } as unknown as ProjectSemanticSnapshot;

    const chunks = buildProjectMeaningChunks(snapshot);
    expect(chunks.map((item) => item.kind)).toEqual([
      'heading', 'prose', 'equation-context', 'figure-caption', 'table-caption', 'theorem',
    ]);
    expect(chunks.find((item) => item.kind === 'prose')?.title).toBe('Methods');
    expect(chunks.find((item) => item.kind === 'figure-caption')?.text).toContain('Robustness');
  });

  it('reuses an unchanged content hash even when source offsets shift', () => {
    const original = chunk('main.tex:100:110:prose', 'unchanged', 100);
    const vectors = new Float32Array(768);
    vectors[0] = 1;
    const index: StoredProjectMeaningIndex = {
      schemaVersion: PROJECT_MEANING_SCHEMA_VERSION,
      generationId: 'generation-1',
      modelId: EMBEDDING_MODEL_ID,
      modelVersion: EMBEDDING_MODEL_VERSION,
      dimensions: 768,
      updatedAt: 1,
      chunks: [original],
      vectors: vectors.buffer,
    };
    const moved = chunk('main.tex:240:250:prose', 'unchanged', 240);
    expect(reusableProjectMeaningVectorIndex(createProjectMeaningReuseLookup(index), moved)).toBe(0);
  });

  it('does not reuse a changed passage merely because its source identity matches', () => {
    const original = chunk('main.tex:100:110:prose', 'old-content', 100);
    const index: StoredProjectMeaningIndex = {
      schemaVersion: PROJECT_MEANING_SCHEMA_VERSION,
      generationId: 'generation-1',
      modelId: EMBEDDING_MODEL_ID,
      modelVersion: EMBEDDING_MODEL_VERSION,
      dimensions: 768,
      updatedAt: 1,
      chunks: [original],
      vectors: new Float32Array(768).buffer,
    };
    const changed = chunk('main.tex:100:110:prose', 'new-content', 100);
    expect(reusableProjectMeaningVectorIndex(createProjectMeaningReuseLookup(index), changed)).toBeUndefined();
  });
});
