/**
 * Unit tests for computeDiff (TRUEREF-0017).
 */

import { describe, it, expect } from 'vitest';
import { computeDiff } from './diff.js';
import type { CrawledFile } from '$lib/server/crawler/types.js';
import type { Document } from '$lib/types';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeCrawledFile(path: string, sha: string): CrawledFile {
	return { path, sha, content: `content of ${path}`, size: 100, language: 'markdown' };
}

function makeDocument(filePath: string, checksum: string): Document {
	return {
		id: `doc-${filePath}`,
		repositoryId: '/test/repo',
		versionId: null,
		filePath,
		title: null,
		language: 'markdown',
		tokenCount: 0,
		checksum,
		indexedAt: new Date()
	};
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('computeDiff', () => {
	it('returns empty buckets when both inputs are empty', () => {
		const diff = computeDiff([], []);
		expect(diff.added).toEqual([]);
		expect(diff.modified).toEqual([]);
		expect(diff.deleted).toEqual([]);
		expect(diff.unchanged).toEqual([]);
	});

	it('classifies all crawled files as added when there are no existing docs', () => {
		const files = [makeCrawledFile('a.md', 'sha-a'), makeCrawledFile('b.md', 'sha-b')];
		const diff = computeDiff(files, []);
		expect(diff.added).toHaveLength(2);
		expect(diff.added.map((f) => f.path)).toEqual(['a.md', 'b.md']);
		expect(diff.modified).toEqual([]);
		expect(diff.deleted).toEqual([]);
		expect(diff.unchanged).toEqual([]);
	});

	it('classifies all DB docs as deleted when crawl returns empty', () => {
		const docs = [makeDocument('a.md', 'sha-a'), makeDocument('b.md', 'sha-b')];
		const diff = computeDiff([], docs);
		expect(diff.deleted).toHaveLength(2);
		expect(diff.deleted).toContain('a.md');
		expect(diff.deleted).toContain('b.md');
		expect(diff.added).toEqual([]);
		expect(diff.modified).toEqual([]);
		expect(diff.unchanged).toEqual([]);
	});

	it('classifies files with matching checksums as unchanged', () => {
		const files = [makeCrawledFile('a.md', 'sha-a')];
		const docs = [makeDocument('a.md', 'sha-a')];
		const diff = computeDiff(files, docs);
		expect(diff.unchanged).toEqual(['a.md']);
		expect(diff.added).toEqual([]);
		expect(diff.modified).toEqual([]);
		expect(diff.deleted).toEqual([]);
	});

	it('classifies files with differing checksums as modified', () => {
		const files = [makeCrawledFile('a.md', 'sha-a-new')];
		const docs = [makeDocument('a.md', 'sha-a-old')];
		const diff = computeDiff(files, docs);
		expect(diff.modified).toHaveLength(1);
		expect(diff.modified[0].path).toBe('a.md');
		expect(diff.added).toEqual([]);
		expect(diff.unchanged).toEqual([]);
		expect(diff.deleted).toEqual([]);
	});

	it('handles a mixed scenario: added, modified, deleted, and unchanged', () => {
		const crawledFiles = [
			makeCrawledFile('unchanged.md', 'sha-same'),   // unchanged
			makeCrawledFile('modified.md', 'sha-new'),     // modified (different sha)
			makeCrawledFile('added.md', 'sha-added')       // added (not in DB)
			// 'deleted.md' is absent from crawl → deleted
		];

		const existingDocs = [
			makeDocument('unchanged.md', 'sha-same'),  // unchanged
			makeDocument('modified.md', 'sha-old'),    // modified
			makeDocument('deleted.md', 'sha-deleted')  // deleted
		];

		const diff = computeDiff(crawledFiles, existingDocs);

		expect(diff.unchanged).toEqual(['unchanged.md']);
		expect(diff.modified.map((f) => f.path)).toEqual(['modified.md']);
		expect(diff.added.map((f) => f.path)).toEqual(['added.md']);
		expect(diff.deleted).toEqual(['deleted.md']);
	});

	it('covers all files: added + modified + deleted + unchanged totals make sense', () => {
		const crawledFiles = [
			makeCrawledFile('a.md', 'sha-a'),
			makeCrawledFile('b.md', 'sha-b-new'),
			makeCrawledFile('c.md', 'sha-c')
		];

		const existingDocs = [
			makeDocument('a.md', 'sha-a'),     // unchanged
			makeDocument('b.md', 'sha-b-old'), // modified
			makeDocument('d.md', 'sha-d')      // deleted
			// 'c.md' is not in DB → added
		];

		const diff = computeDiff(crawledFiles, existingDocs);

		// added: c.md
		expect(diff.added.map((f) => f.path)).toContain('c.md');
		// modified: b.md
		expect(diff.modified.map((f) => f.path)).toContain('b.md');
		// deleted: d.md
		expect(diff.deleted).toContain('d.md');
		// unchanged: a.md
		expect(diff.unchanged).toContain('a.md');

		// Total accounted for from crawl side = added + modified + unchanged
		const crawlAccountedFor = diff.added.length + diff.modified.length + diff.unchanged.length;
		expect(crawlAccountedFor).toBe(crawledFiles.length);
	});

	it('preserves the full CrawledFile object in added and modified buckets', () => {
		const file = makeCrawledFile('new.ts', 'sha-new');
		const diff = computeDiff([file], []);
		expect(diff.added[0]).toBe(file);
	});

	it('preserves the full CrawledFile object for modified files', () => {
		const file = makeCrawledFile('changed.ts', 'sha-changed');
		const doc = makeDocument('changed.ts', 'sha-original');
		const diff = computeDiff([file], [doc]);
		expect(diff.modified[0]).toBe(file);
	});
});
