/**
 * Unit tests for config-parser.ts and config-validator.ts (TRUEREF-0013).
 */

import { describe, it, expect } from 'vitest';
import { parseConfigFile, resolveConfig, ConfigParseError } from './config-parser.js';
import { validateConfig } from './config-validator.js';

// ---------------------------------------------------------------------------
// parseConfigFile — structural errors (throw)
// ---------------------------------------------------------------------------

describe('parseConfigFile — structural errors', () => {
	it('throws ConfigParseError on invalid JSON', () => {
		expect(() => parseConfigFile('not json', 'trueref.json')).toThrowError(ConfigParseError);
		expect(() => parseConfigFile('not json', 'trueref.json')).toThrowError(/not valid JSON/);
	});

	it('throws ConfigParseError when root is an array', () => {
		expect(() => parseConfigFile('[]', 'trueref.json')).toThrowError(ConfigParseError);
		expect(() => parseConfigFile('[]', 'trueref.json')).toThrowError(/must be a JSON object/);
	});

	it('throws ConfigParseError when root is a string', () => {
		expect(() => parseConfigFile('"hello"', 'trueref.json')).toThrowError(ConfigParseError);
	});

	it('throws ConfigParseError when root is null', () => {
		expect(() => parseConfigFile('null', 'trueref.json')).toThrowError(ConfigParseError);
	});

	it('throws ConfigParseError when root is a number', () => {
		expect(() => parseConfigFile('42', 'trueref.json')).toThrowError(ConfigParseError);
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — valid minimal input
// ---------------------------------------------------------------------------

describe('parseConfigFile — minimal valid input', () => {
	it('accepts an empty object with no warnings', () => {
		const result = parseConfigFile('{}', 'trueref.json');
		expect(result.config).toEqual({});
		expect(result.warnings).toHaveLength(0);
		expect(result.source).toBe('trueref.json');
	});

	it('sets source to context7.json for context7.json filename', () => {
		const result = parseConfigFile('{}', 'context7.json');
		expect(result.source).toBe('context7.json');
	});

	it('source is trueref.json for case-insensitive match', () => {
		const result = parseConfigFile('{}', 'TRUEREF.json');
		expect(result.source).toBe('trueref.json');
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — projectTitle
// ---------------------------------------------------------------------------

describe('parseConfigFile — projectTitle', () => {
	it('accepts a valid projectTitle', () => {
		const result = parseConfigFile(JSON.stringify({ projectTitle: 'My Library' }), 'trueref.json');
		expect(result.config.projectTitle).toBe('My Library');
		expect(result.warnings).toHaveLength(0);
	});

	it('truncates projectTitle exceeding 100 characters and warns', () => {
		const long = 'a'.repeat(150);
		const result = parseConfigFile(JSON.stringify({ projectTitle: long }), 'trueref.json');
		expect(result.config.projectTitle).toHaveLength(100);
		expect(result.warnings.some((w) => /projectTitle truncated/.test(w))).toBe(true);
	});

	it('ignores non-string projectTitle with a warning', () => {
		const result = parseConfigFile(JSON.stringify({ projectTitle: 42 }), 'trueref.json');
		expect(result.config.projectTitle).toBeUndefined();
		expect(result.warnings.some((w) => /projectTitle must be a string/.test(w))).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — description
// ---------------------------------------------------------------------------

describe('parseConfigFile — description', () => {
	it('accepts a valid description', () => {
		const result = parseConfigFile(
			JSON.stringify({ description: 'A useful library for things.' }),
			'trueref.json'
		);
		expect(result.config.description).toBe('A useful library for things.');
	});

	it('truncates description exceeding 500 characters', () => {
		const long = 'x'.repeat(600);
		const result = parseConfigFile(JSON.stringify({ description: long }), 'trueref.json');
		expect(result.config.description).toHaveLength(500);
		expect(result.warnings.some((w) => /description truncated/.test(w))).toBe(true);
	});

	it('ignores non-string description with a warning', () => {
		const result = parseConfigFile(JSON.stringify({ description: true }), 'trueref.json');
		expect(result.config.description).toBeUndefined();
		expect(result.warnings.some((w) => /description must be a string/.test(w))).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — folders / excludeFolders / excludeFiles
// ---------------------------------------------------------------------------

describe('parseConfigFile — array path fields', () => {
	it('accepts valid folders', () => {
		const result = parseConfigFile(
			JSON.stringify({ folders: ['src/', 'docs/'] }),
			'trueref.json'
		);
		expect(result.config.folders).toEqual(['src/', 'docs/']);
		expect(result.warnings).toHaveLength(0);
	});

	it('ignores non-array folders with a warning', () => {
		const result = parseConfigFile(JSON.stringify({ folders: 'src/' }), 'trueref.json');
		expect(result.config.folders).toBeUndefined();
		expect(result.warnings.some((w) => /folders must be an array/.test(w))).toBe(true);
	});

	it('skips non-string entries in folders with a warning', () => {
		const result = parseConfigFile(
			JSON.stringify({ folders: ['src/', 42, true] }),
			'trueref.json'
		);
		expect(result.config.folders).toEqual(['src/']);
		expect(result.warnings.length).toBeGreaterThan(0);
	});

	it('drops folders entries beyond maxItems (50) and warns', () => {
		const tooMany = Array.from({ length: 60 }, (_, i) => `folder${i}/`);
		const result = parseConfigFile(JSON.stringify({ folders: tooMany }), 'trueref.json');
		expect(result.config.folders).toHaveLength(50);
		expect(result.warnings.some((w) => /extras dropped/.test(w))).toBe(true);
	});

	it('truncates folder entries exceeding 200 characters', () => {
		const longPath = 'a'.repeat(250);
		const result = parseConfigFile(JSON.stringify({ folders: [longPath] }), 'trueref.json');
		expect(result.config.folders![0]).toHaveLength(200);
	});

	it('drops excludeFiles entries beyond maxItems (100) and warns', () => {
		const tooMany = Array.from({ length: 110 }, (_, i) => `file${i}.ts`);
		const result = parseConfigFile(JSON.stringify({ excludeFiles: tooMany }), 'trueref.json');
		expect(result.config.excludeFiles).toHaveLength(100);
		expect(result.warnings.some((w) => /extras dropped/.test(w))).toBe(true);
	});

	it('accepts valid excludeFiles', () => {
		const result = parseConfigFile(
			JSON.stringify({ excludeFiles: ['README.md', 'CHANGELOG.md'] }),
			'trueref.json'
		);
		expect(result.config.excludeFiles).toEqual(['README.md', 'CHANGELOG.md']);
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — rules
// ---------------------------------------------------------------------------

describe('parseConfigFile — rules', () => {
	it('accepts valid rules', () => {
		const result = parseConfigFile(
			JSON.stringify({ rules: ['Always use named imports.', 'Prefer async/await over callbacks.'] }),
			'trueref.json'
		);
		expect(result.config.rules).toHaveLength(2);
	});

	it('drops rules shorter than 5 characters with a warning', () => {
		const result = parseConfigFile(
			JSON.stringify({ rules: ['ok', 'This rule is valid.'] }),
			'trueref.json'
		);
		expect(result.config.rules).toHaveLength(1);
		expect(result.config.rules![0]).toBe('This rule is valid.');
		expect(result.warnings.some((w) => /too short/.test(w))).toBe(true);
	});

	it('truncates rules exceeding 500 characters', () => {
		const longRule = 'x'.repeat(600);
		const result = parseConfigFile(JSON.stringify({ rules: [longRule] }), 'trueref.json');
		expect(result.config.rules![0]).toHaveLength(500);
	});

	it('drops rules beyond maxItems (20) and warns', () => {
		const tooMany = Array.from({ length: 25 }, (_, i) => `Rule number ${i + 1} that is valid.`);
		const result = parseConfigFile(JSON.stringify({ rules: tooMany }), 'trueref.json');
		expect(result.config.rules).toHaveLength(20);
		expect(result.warnings.some((w) => /extras dropped/.test(w))).toBe(true);
	});

	it('ignores non-array rules with a warning', () => {
		const result = parseConfigFile(
			JSON.stringify({ rules: 'use named imports' }),
			'trueref.json'
		);
		expect(result.config.rules).toBeUndefined();
		expect(result.warnings.some((w) => /rules must be an array/.test(w))).toBe(true);
	});

	it('skips non-string rule entries with a warning', () => {
		const result = parseConfigFile(
			JSON.stringify({ rules: ['Valid rule here.', 99, null] }),
			'trueref.json'
		);
		expect(result.config.rules).toHaveLength(1);
	});
});

// ---------------------------------------------------------------------------
// parseConfigFile — previousVersions
// ---------------------------------------------------------------------------

describe('parseConfigFile — previousVersions', () => {
	it('accepts valid previousVersions', () => {
		const result = parseConfigFile(
			JSON.stringify({
				previousVersions: [
					{ tag: 'v1.2.3', title: 'Version 1.2.3' },
					{ tag: '2.0.0', title: 'Version 2.0.0' }
				]
			}),
			'trueref.json'
		);
		expect(result.config.previousVersions).toHaveLength(2);
		expect(result.config.previousVersions![0].tag).toBe('v1.2.3');
	});

	it('skips entries missing tag', () => {
		const result = parseConfigFile(
			JSON.stringify({
				previousVersions: [
					{ title: 'No tag here' },
					{ tag: 'v1.0.0', title: 'Valid' }
				]
			}),
			'trueref.json'
		);
		expect(result.config.previousVersions).toHaveLength(1);
		expect(result.warnings.length).toBeGreaterThan(0);
	});

	it('skips entries missing title', () => {
		const result = parseConfigFile(
			JSON.stringify({
				previousVersions: [{ tag: 'v2.0.0' }, { tag: 'v1.0.0', title: 'Valid' }]
			}),
			'trueref.json'
		);
		expect(result.config.previousVersions).toHaveLength(1);
	});

	it('drops entries beyond maxItems (50) and warns', () => {
		const tooMany = Array.from({ length: 55 }, (_, i) => ({
			tag: `v1.${i}.0`,
			title: `Version 1.${i}.0`
		}));
		const result = parseConfigFile(JSON.stringify({ previousVersions: tooMany }), 'trueref.json');
		expect(result.config.previousVersions).toHaveLength(50);
		expect(result.warnings.some((w) => /extras dropped/.test(w))).toBe(true);
	});

	it('ignores non-array previousVersions with a warning', () => {
		const result = parseConfigFile(
			JSON.stringify({ previousVersions: 'v1.0.0' }),
			'trueref.json'
		);
		expect(result.config.previousVersions).toBeUndefined();
		expect(result.warnings.some((w) => /previousVersions must be an array/.test(w))).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// resolveConfig
// ---------------------------------------------------------------------------

describe('resolveConfig', () => {
	it('returns null when no candidates', () => {
		expect(resolveConfig([])).toBeNull();
	});

	it('returns null when no matching filenames', () => {
		expect(
			resolveConfig([{ filename: 'package.json', content: '{"name":"x"}' }])
		).toBeNull();
	});

	it('prefers trueref.json over context7.json', () => {
		const result = resolveConfig([
			{ filename: 'context7.json', content: JSON.stringify({ projectTitle: 'Context7 Title' }) },
			{ filename: 'trueref.json', content: JSON.stringify({ projectTitle: 'TrueRef Title' }) }
		]);
		expect(result).not.toBeNull();
		expect(result!.source).toBe('trueref.json');
		expect(result!.config.projectTitle).toBe('TrueRef Title');
	});

	it('falls back to context7.json when trueref.json is absent', () => {
		const result = resolveConfig([
			{ filename: 'context7.json', content: JSON.stringify({ projectTitle: 'C7 Library' }) }
		]);
		expect(result).not.toBeNull();
		expect(result!.source).toBe('context7.json');
		expect(result!.config.projectTitle).toBe('C7 Library');
	});

	it('uses trueref.json even when listed second', () => {
		const result = resolveConfig([
			{ filename: 'package.json', content: '{}' },
			{ filename: 'trueref.json', content: JSON.stringify({ projectTitle: 'My Lib' }) },
			{ filename: 'context7.json', content: JSON.stringify({ projectTitle: 'Old' }) }
		]);
		expect(result!.source).toBe('trueref.json');
		expect(result!.config.projectTitle).toBe('My Lib');
	});
});

// ---------------------------------------------------------------------------
// validateConfig
// ---------------------------------------------------------------------------

describe('validateConfig', () => {
	it('returns valid=true with no errors for an empty config', () => {
		const result = validateConfig({});
		expect(result.valid).toBe(true);
		expect(result.errors).toHaveLength(0);
	});

	it('returns valid=true for a fully populated correct config', () => {
		const result = validateConfig({
			projectTitle: 'My Library',
			description: 'A library that does useful things very well.',
			folders: ['src/', 'docs/'],
			excludeFolders: ['test/'],
			excludeFiles: ['README.md'],
			rules: ['Always use named imports.', 'Check the docs before opening issues.'],
			previousVersions: [{ tag: 'v1.2.3', title: 'Version 1.2.3' }]
		});
		expect(result.valid).toBe(true);
		expect(result.errors).toHaveLength(0);
	});

	it('returns error for projectTitle shorter than 1 character', () => {
		const result = validateConfig({ projectTitle: '' });
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /projectTitle/.test(e))).toBe(true);
	});

	it('returns error for description shorter than 10 characters', () => {
		const result = validateConfig({ description: 'Short' });
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /description/.test(e))).toBe(true);
	});

	it('returns error for excludeFiles entry with a path separator', () => {
		const result = validateConfig({ excludeFiles: ['src/index.ts'] });
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /excludeFiles/.test(e))).toBe(true);
	});

	it('returns error for a rule shorter than 5 characters', () => {
		// The parser already filters these out; the validator catches them if
		// config is constructed directly (e.g. from DB or programmatic use).
		const result = validateConfig({ rules: ['hi'] });
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /rules/.test(e))).toBe(true);
	});

	it('returns error for invalid version tag pattern', () => {
		const result = validateConfig({
			previousVersions: [{ tag: 'not-a-semver', title: 'Some Version' }]
		});
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /tag/.test(e))).toBe(true);
	});

	it('returns error for version with empty title', () => {
		const result = validateConfig({
			previousVersions: [{ tag: 'v1.0.0', title: '' }]
		});
		expect(result.valid).toBe(false);
		expect(result.errors.some((e) => /title/.test(e))).toBe(true);
	});

	it('returns warning for oversized arrays (post-parse, pre-DB state)', () => {
		// Build a rules array with 25 items to trigger the >20 warning.
		const rules = Array.from({ length: 25 }, (_, i) => `Valid rule number ${i + 1} here.`);
		const result = validateConfig({ rules });
		// Validator warns but does not error for count overages.
		expect(result.warnings.some((w) => /rules/.test(w))).toBe(true);
	});

	it('accepts version tags without v prefix', () => {
		const result = validateConfig({
			previousVersions: [{ tag: '2.0.0', title: 'Version 2' }]
		});
		expect(result.valid).toBe(true);
	});

	it('accepts pre-release version tags', () => {
		const result = validateConfig({
			previousVersions: [{ tag: 'v2.0.0-beta.1', title: 'Beta Release' }]
		});
		expect(result.valid).toBe(true);
	});
});

// ---------------------------------------------------------------------------
// Integration: parse then validate
// ---------------------------------------------------------------------------

describe('parse then validate integration', () => {
	it('a well-formed trueref.json passes both parse and validate', () => {
		const content = JSON.stringify({
			projectTitle: 'Awesome Library',
			description: 'This library does awesome things for awesome developers.',
			folders: ['src/', 'docs/'],
			excludeFolders: ['test/', '__mocks__/'],
			excludeFiles: ['jest.config.ts', 'vitest.config.ts'],
			rules: [
				'Always import types with the `type` keyword.',
				'Use the factory function, not the constructor directly.'
			],
			previousVersions: [
				{ tag: 'v1.0.0', title: 'Initial Release' },
				{ tag: 'v1.2.0', title: 'Stable Release' }
			]
		});

		const parsed = parseConfigFile(content, 'trueref.json');
		expect(parsed.warnings).toHaveLength(0);

		const validated = validateConfig(parsed.config);
		expect(validated.valid).toBe(true);
		expect(validated.errors).toHaveLength(0);
	});

	it('a context7.json is parsed and validated identically', () => {
		const content = JSON.stringify({
			projectTitle: 'My Context7 Library',
			rules: ['Prefer composition over inheritance.']
		});

		const parsed = parseConfigFile(content, 'context7.json');
		expect(parsed.source).toBe('context7.json');

		const validated = validateConfig(parsed.config);
		expect(validated.valid).toBe(true);
	});
});
