/**
 * Unit tests for RepositoryService.
 *
 * The service uses raw better-sqlite3 queries, so returned rows have
 * snake_case column names matching the SQLite schema. Tests assert against
 * those actual runtime keys.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import Database from 'better-sqlite3';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { RepositoryService } from './repository.service';
import {
	AlreadyExistsError,
	InvalidInputError,
	InvalidUrlError,
	NotFoundError
} from '$lib/server/utils/validation';

// ---------------------------------------------------------------------------
// Test DB factory
// ---------------------------------------------------------------------------

function createTestDb(): Database.Database {
	const client = new Database(':memory:');
	client.pragma('foreign_keys = ON');

	const migrationsFolder = join(import.meta.dirname, '../db/migrations');
	const migrationSql = readFileSync(
		join(migrationsFolder, '0000_large_master_chief.sql'),
		'utf-8'
	);

	// Drizzle migration files use `--> statement-breakpoint` as separator.
	const statements = migrationSql
		.split('--> statement-breakpoint')
		.map((s) => s.trim())
		.filter(Boolean);

	for (const stmt of statements) {
		client.exec(stmt);
	}

	return client;
}

// Raw row shape returned by better-sqlite3 SELECT * FROM repositories.
interface RawRepo {
	id: string;
	title: string;
	description: string | null;
	source: string;
	source_url: string;
	branch: string | null;
	state: string;
	total_snippets: number;
	total_tokens: number;
	trust_score: number;
	benchmark_score: number;
	stars: number | null;
	github_token: string | null;
	last_indexed_at: number | null;
	created_at: number;
	updated_at: number;
}

// Raw row shape returned by better-sqlite3 SELECT * FROM indexing_jobs.
interface RawJob {
	id: string;
	repository_id: string;
	version_id: string | null;
	status: string;
	progress: number;
	total_files: number;
	processed_files: number;
	error: string | null;
	started_at: number | null;
	completed_at: number | null;
	created_at: number;
}

function makeService(client: Database.Database): RepositoryService {
	return new RepositoryService(client);
}

// ---------------------------------------------------------------------------
// list() and count()
// ---------------------------------------------------------------------------

describe('RepositoryService.list()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
	});

	it('returns empty array when no repositories exist', () => {
		const result = service.list();
		expect(result).toEqual([]);
	});

	it('returns all repositories after adding some', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/a' });
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/b' });
		const result = service.list();
		expect(result).toHaveLength(2);
	});

	it('filters by state', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/a' });
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/b' });
		// All repos start as 'pending'.
		const pending = service.list({ state: 'pending' });
		expect(pending).toHaveLength(2);
		const indexed = service.list({ state: 'indexed' });
		expect(indexed).toHaveLength(0);
	});

	it('respects limit and offset', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/a' });
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/b' });
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/c' });
		const page1 = service.list({ limit: 2, offset: 0 });
		const page2 = service.list({ limit: 2, offset: 2 });
		expect(page1).toHaveLength(2);
		expect(page2).toHaveLength(1);
	});
});

describe('RepositoryService.count()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
	});

	it('returns 0 when empty', () => {
		expect(service.count()).toBe(0);
	});

	it('returns correct count after adding repositories', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/a' });
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/b' });
		expect(service.count()).toBe(2);
	});

	it('filters count by state', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/foo/a' });
		expect(service.count('pending')).toBe(1);
		expect(service.count('indexed')).toBe(0);
	});
});

// ---------------------------------------------------------------------------
// get()
// ---------------------------------------------------------------------------

describe('RepositoryService.get()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
	});

	it('returns null for a non-existent repository', () => {
		expect(service.get('/not/found')).toBeNull();
	});

	it('returns the repository when it exists', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
		const repo = service.get('/facebook/react') as unknown as RawRepo;
		expect(repo).not.toBeNull();
		expect(repo.id).toBe('/facebook/react');
		expect(repo.source).toBe('github');
	});
});

// ---------------------------------------------------------------------------
// add()
// ---------------------------------------------------------------------------

describe('RepositoryService.add()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
	});

	it('adds a GitHub repository and derives the correct ID', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react'
		}) as unknown as RawRepo;
		expect(repo.id).toBe('/facebook/react');
		expect(repo.source).toBe('github');
		expect(repo.state).toBe('pending');
	});

	it('accepts github.com/owner/repo URL format', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'github.com/facebook/react'
		});
		expect(repo.id).toBe('/facebook/react');
	});

	it('strips .git suffix from GitHub URLs', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react.git'
		});
		expect(repo.id).toBe('/facebook/react');
	});

	it('uses override title when provided', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react',
			title: 'React Library'
		});
		expect(repo.title).toBe('React Library');
	});

	it('defaults title to repo name for GitHub repos', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react'
		});
		expect(repo.title).toBe('react');
	});

	it('adds a local repository and derives the /local/ ID', () => {
		const repo = service.add({
			source: 'local',
			sourceUrl: '/home/user/projects/my-sdk'
		}) as unknown as RawRepo;
		expect(repo.id).toBe('/local/my-sdk');
		expect(repo.source).toBe('local');
	});

	it('resolves ID collisions for local repositories with -2, -3 suffix', () => {
		service.add({ source: 'local', sourceUrl: '/home/user/a/my-sdk' });
		const repo2 = service.add({ source: 'local', sourceUrl: '/home/user/b/my-sdk' });
		expect(repo2.id).toBe('/local/my-sdk-2');
		const repo3 = service.add({ source: 'local', sourceUrl: '/home/user/c/my-sdk' });
		expect(repo3.id).toBe('/local/my-sdk-3');
	});

	it('throws AlreadyExistsError when a GitHub repo already exists', () => {
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
		expect(() =>
			service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' })
		).toThrow(AlreadyExistsError);
	});

	it('throws InvalidUrlError for a bad GitHub URL', () => {
		expect(() =>
			service.add({ source: 'github', sourceUrl: 'https://gitlab.com/foo/bar' })
		).toThrow(InvalidUrlError);
	});

	it('throws InvalidInputError when sourceUrl is empty', () => {
		expect(() =>
			service.add({ source: 'github', sourceUrl: '' })
		).toThrow(InvalidInputError);
	});

	it('stores description and branch when provided', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react',
			description: 'A UI library',
			branch: 'main'
		}) as unknown as RawRepo;
		expect(repo.description).toBe('A UI library');
		expect(repo.branch).toBe('main');
	});

	it('initialises counters to zero', () => {
		const repo = service.add({
			source: 'github',
			sourceUrl: 'https://github.com/facebook/react'
		}) as unknown as RawRepo;
		expect(repo.total_snippets).toBe(0);
		expect(repo.total_tokens).toBe(0);
	});
});

// ---------------------------------------------------------------------------
// update()
// ---------------------------------------------------------------------------

describe('RepositoryService.update()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
	});

	it('updates the title', () => {
		const updated = service.update('/facebook/react', { title: 'React' });
		expect(updated.title).toBe('React');
	});

	it('updates the description', () => {
		const updated = service.update('/facebook/react', { description: 'UI library' });
		expect(updated.description).toBe('UI library');
	});

	it('updates the branch', () => {
		const updated = service.update('/facebook/react', { branch: 'canary' }) as unknown as RawRepo;
		expect(updated.branch).toBe('canary');
	});

	it('returns unchanged repository when input has no fields', () => {
		const before = service.get('/facebook/react')!;
		const updated = service.update('/facebook/react', {});
		expect(updated.title).toBe(before.title);
	});

	it('throws NotFoundError for a non-existent repository', () => {
		expect(() =>
			service.update('/not/found', { title: 'New Title' })
		).toThrow(NotFoundError);
	});
});

// ---------------------------------------------------------------------------
// remove()
// ---------------------------------------------------------------------------

describe('RepositoryService.remove()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
	});

	it('removes an existing repository', () => {
		service.remove('/facebook/react');
		expect(service.get('/facebook/react')).toBeNull();
	});

	it('throws NotFoundError when the repository does not exist', () => {
		expect(() => service.remove('/not/found')).toThrow(NotFoundError);
	});
});

// ---------------------------------------------------------------------------
// getStats()
// ---------------------------------------------------------------------------

describe('RepositoryService.getStats()', () => {
	let client: Database.Database;
	let service: RepositoryService;

	beforeEach(() => {
		client = createTestDb();
		service = makeService(client);
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
	});

	it('returns zero stats for a freshly added repository', () => {
		const stats = service.getStats('/facebook/react');
		expect(stats.totalSnippets).toBe(0);
		expect(stats.totalTokens).toBe(0);
		expect(stats.totalDocuments).toBe(0);
		// lastIndexedAt may be undefined or null depending on the raw row value.
		expect(stats.lastIndexedAt == null).toBe(true);
	});

	it('throws NotFoundError for a non-existent repository', () => {
		expect(() => service.getStats('/not/found')).toThrow(NotFoundError);
	});

	it('counts documents correctly', () => {
		const docId = crypto.randomUUID();
		const now = Math.floor(Date.now() / 1000);
		client
			.prepare(
				`INSERT INTO documents (id, repository_id, file_path, checksum, indexed_at)
         VALUES (?, '/facebook/react', 'README.md', 'abc', ?)`
			)
			.run(docId, now);

		const stats = service.getStats('/facebook/react');
		expect(stats.totalDocuments).toBe(1);
	});
});

// ---------------------------------------------------------------------------
// getVersions()
// ---------------------------------------------------------------------------

describe('RepositoryService.getVersions()', () => {
	let client: Database.Database;
	let service: RepositoryService;

	beforeEach(() => {
		client = createTestDb();
		service = makeService(client);
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
	});

	it('returns an empty array when no versions exist', () => {
		expect(service.getVersions('/facebook/react')).toEqual([]);
	});

	it('returns tags for all versions of a repository', () => {
		const now = Math.floor(Date.now() / 1000);
		client
			.prepare(
				`INSERT INTO repository_versions (id, repository_id, tag, created_at)
         VALUES (?, '/facebook/react', ?, ?)`
			)
			.run('/facebook/react/v18.3.0', 'v18.3.0', now);
		client
			.prepare(
				`INSERT INTO repository_versions (id, repository_id, tag, created_at)
         VALUES (?, '/facebook/react', ?, ?)`
			)
			.run('/facebook/react/v17.0.2', 'v17.0.2', now - 1);

		const versions = service.getVersions('/facebook/react');
		expect(versions).toContain('v18.3.0');
		expect(versions).toContain('v17.0.2');
	});
});

// ---------------------------------------------------------------------------
// createIndexingJob()
// ---------------------------------------------------------------------------

describe('RepositoryService.createIndexingJob()', () => {
	let service: RepositoryService;

	beforeEach(() => {
		service = makeService(createTestDb());
		service.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
	});

	it('creates a queued indexing job', () => {
		const job = service.createIndexingJob('/facebook/react') as unknown as RawJob;
		expect(job.id).toBeTruthy();
		expect(job.repository_id).toBe('/facebook/react');
		expect(job.status).toBe('queued');
		expect(job.progress).toBe(0);
	});

	it('returns the existing job when one is already queued', () => {
		const job1 = service.createIndexingJob('/facebook/react');
		const job2 = service.createIndexingJob('/facebook/react');
		expect(job2.id).toBe(job1.id);
	});

	it('creates a new job once the previous job is done', () => {
		const job1 = service.createIndexingJob('/facebook/react');
		// Access the underlying db to simulate job completion.
		const db = (service as unknown as { db: Database.Database }).db;
		db.prepare(`UPDATE indexing_jobs SET status = 'done' WHERE id = ?`).run(job1.id);

		const job2 = service.createIndexingJob('/facebook/react');
		expect(job2.id).not.toBe(job1.id);
	});

	it('accepts an optional versionId', () => {
		const job = service.createIndexingJob(
			'/facebook/react',
			'/facebook/react/v18.3.0'
		) as unknown as RawJob;
		expect(job.version_id).toBe('/facebook/react/v18.3.0');
	});
});
