/**
 * Unit tests for FileSystemProjectScaffolder.
 *
 * Drives against a real temp directory so end-to-end file IO + path-traversal
 * defenses are exercised.
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
    FileSystemProjectScaffolder,
    validateProjectName,
} from '../src/scaffolder/projectScaffolder';

function tmp(): string {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'astra-scaffold-test-'));
}

describe('validateProjectName', () => {
    test('accepts allowed names', () => {
        expect(validateProjectName('foo')).toBe('foo');
        expect(validateProjectName('foo-bar_v2')).toBe('foo-bar_v2');
        expect(validateProjectName('  trimmed  ')).toBe('trimmed');
    });

    test('rejects too short', () => {
        expect(validateProjectName('a')).toBeNull();
    });

    test('rejects too long', () => {
        expect(validateProjectName('a'.repeat(41))).toBeNull();
    });

    test('rejects invalid chars', () => {
        expect(validateProjectName('foo bar')).toBeNull();
        expect(validateProjectName('foo/bar')).toBeNull();
        expect(validateProjectName('foo.bar')).toBeNull();
        expect(validateProjectName('한글이름')).toBeNull();
    });

    test('rejects empty / non-string', () => {
        expect(validateProjectName('')).toBeNull();
        expect(validateProjectName(undefined as any)).toBeNull();
    });
});

describe('FileSystemProjectScaffolder', () => {
    let root: string;
    let scaffolder: FileSystemProjectScaffolder;

    beforeEach(() => {
        root = tmp();
        scaffolder = new FileSystemProjectScaffolder();
    });

    afterEach(() => {
        try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
    });

    test('listTemplates returns the catalog', () => {
        const list = scaffolder.listTemplates();
        const ids = list.map(t => t.id);
        expect(ids).toContain('static');
        expect(ids).toContain('vite-vanilla');
        expect(ids).toContain('vite-react');
    });

    test('static template writes index.html + README.md', async () => {
        const result = await scaffolder.scaffold({ name: 'demo-static', template: 'static', rootDir: root });
        expect(result.ok).toBe(true);
        if (!result.ok) return;
        expect(fs.existsSync(path.join(result.projectPath, 'site', 'index.html'))).toBe(true);
        expect(fs.existsSync(path.join(result.projectPath, 'README.md'))).toBe(true);
        const html = fs.readFileSync(path.join(result.projectPath, 'site', 'index.html'), 'utf8');
        expect(html).toContain('demo-static');
    });

    test('vite-vanilla template writes package.json + main.js', async () => {
        const result = await scaffolder.scaffold({ name: 'vv', template: 'vite-vanilla', rootDir: root });
        expect(result.ok).toBe(true);
        if (!result.ok) return;
        const pkg = JSON.parse(fs.readFileSync(path.join(result.projectPath, 'site', 'package.json'), 'utf8'));
        expect(pkg.name).toBe('vv');
        expect(pkg.devDependencies.vite).toBeDefined();
        expect(fs.existsSync(path.join(result.projectPath, 'site', 'main.js'))).toBe(true);
    });

    test('vite-react template includes tsconfig + main.tsx', async () => {
        const result = await scaffolder.scaffold({ name: 'vr', template: 'vite-react', rootDir: root });
        expect(result.ok).toBe(true);
        if (!result.ok) return;
        expect(fs.existsSync(path.join(result.projectPath, 'site', 'tsconfig.json'))).toBe(true);
        expect(fs.existsSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'))).toBe(true);
        const tsx = fs.readFileSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'), 'utf8');
        expect(tsx).toContain('<h1>vr</h1>');
    });

    test('rejects invalid name with INVALID_NAME', async () => {
        const result = await scaffolder.scaffold({ name: 'foo bar', template: 'static', rootDir: root });
        expect(result).toEqual(expect.objectContaining({ ok: false, code: 'INVALID_NAME' }));
    });

    test('rejects unknown template with UNKNOWN_TEMPLATE', async () => {
        const result = await scaffolder.scaffold({ name: 'foo', template: 'made-up' as any, rootDir: root });
        expect(result).toEqual(expect.objectContaining({ ok: false, code: 'UNKNOWN_TEMPLATE' }));
    });

    test('rejects empty rootDir with NO_ROOT_DIR', async () => {
        const result = await scaffolder.scaffold({ name: 'foo', template: 'static', rootDir: '' });
        expect(result).toEqual(expect.objectContaining({ ok: false, code: 'NO_ROOT_DIR' }));
    });

    test('rejects relative rootDir with ROOT_NOT_ABSOLUTE', async () => {
        const result = await scaffolder.scaffold({ name: 'foo', template: 'static', rootDir: 'relative/path' });
        expect(result).toEqual(expect.objectContaining({ ok: false, code: 'ROOT_NOT_ABSOLUTE' }));
    });

    test('rejects when target already exists with ALREADY_EXISTS', async () => {
        const r1 = await scaffolder.scaffold({ name: 'twice', template: 'static', rootDir: root });
        expect(r1.ok).toBe(true);
        const r2 = await scaffolder.scaffold({ name: 'twice', template: 'static', rootDir: root });
        expect(r2).toEqual(expect.objectContaining({ ok: false, code: 'ALREADY_EXISTS' }));
    });

    test('reports filesWritten for downstream UI', async () => {
        const result = await scaffolder.scaffold({ name: 'count', template: 'vite-react', rootDir: root });
        expect(result.ok).toBe(true);
        if (!result.ok) return;
        expect(result.filesWritten.length).toBeGreaterThanOrEqual(5);
        for (const file of result.filesWritten) {
            expect(fs.existsSync(file)).toBe(true);
        }
    });
});
