/**
 * Unit tests for FileSystemSkillInjectionService.
 *
 * Strategy: drive the service against a real temp directory so path-traversal
 * defenses and writeFileSync paths are exercised end-to-end. The service
 * accepts a fs override but the prod path uses node:fs — testing real fs
 * gives stronger confidence here.
 */

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
    FileSystemSkillInjectionService,
    SkillInjectionError,
    sanitizeSkillName,
} from '../src/skills/skillInjectionService';

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

describe('sanitizeSkillName', () => {
    test('keeps ASCII alphanumerics and -_.', () => {
        expect(sanitizeSkillName('frontend_expert-v2.alpha')).toBe('frontend_expert-v2.alpha');
    });

    test('strips .md extension', () => {
        expect(sanitizeSkillName('foo.md')).toBe('foo');
        expect(sanitizeSkillName('foo.markdown')).toBe('foo');
    });

    test('replaces invalid chars with underscore', () => {
        expect(sanitizeSkillName('hello world!')).toBe('hello_world');
        expect(sanitizeSkillName('foo/bar')).toBe('foo_bar');
        expect(sanitizeSkillName('파이썬')).toBe('');
    });

    test('strips leading/trailing dots and underscores', () => {
        expect(sanitizeSkillName('___foo___')).toBe('foo');
        expect(sanitizeSkillName('...foo...')).toBe('foo');
    });

    test('returns empty for path-traversal attempts', () => {
        // ".." after stripping leading dots collapses to ""
        expect(sanitizeSkillName('..')).toBe('');
        expect(sanitizeSkillName('../etc/passwd')).toBe('etc_passwd');
    });

    test('caps length at 80', () => {
        const long = 'a'.repeat(200);
        expect(sanitizeSkillName(long).length).toBe(80);
    });

    test('rejects blank / non-string input', () => {
        expect(sanitizeSkillName('')).toBe('');
        expect(sanitizeSkillName('   ')).toBe('');
        expect(sanitizeSkillName(undefined as any)).toBe('');
    });
});

describe('FileSystemSkillInjectionService.inject', () => {
    let dir: string;
    let service: FileSystemSkillInjectionService;

    beforeEach(() => {
        dir = makeTempDir();
        service = new FileSystemSkillInjectionService({
            resolveSkillsDir: () => dir,
        });
    });

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

    test('writes <name>.md and <name>.meta.json', async () => {
        const result = await service.inject({
            name: 'react_expert',
            content: '# React Expert\n\nKnow React deeply.',
            displayName: 'React Expert',
            description: 'React specialist agent.',
            source: 'ezer',
        });
        expect(result.safeName).toBe('react_expert');
        expect(fs.existsSync(result.filePath)).toBe(true);
        expect(fs.existsSync(result.metaPath)).toBe(true);

        const md = fs.readFileSync(result.filePath, 'utf8');
        expect(md).toContain('# React Expert');

        const meta = JSON.parse(fs.readFileSync(result.metaPath, 'utf8'));
        expect(meta.name).toBe('react_expert');
        expect(meta.displayName).toBe('React Expert');
        expect(meta.description).toBe('React specialist agent.');
        expect(meta.injectedFrom).toBe('ezer');
        expect(meta.injectedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
    });

    test('sanitizes the name on disk', async () => {
        const result = await service.inject({
            name: 'hello world!.md',
            content: 'body',
        });
        expect(result.safeName).toBe('hello_world');
        expect(path.basename(result.filePath)).toBe('hello_world.md');
    });

    test('rejects empty name', async () => {
        await expect(service.inject({ name: '', content: 'body' }))
            .rejects.toMatchObject({ code: 'INVALID_NAME' });
    });

    test('rejects empty content', async () => {
        await expect(service.inject({ name: 'foo', content: '' }))
            .rejects.toMatchObject({ code: 'EMPTY_CONTENT' });
        await expect(service.inject({ name: 'foo', content: '   ' }))
            .rejects.toMatchObject({ code: 'EMPTY_CONTENT' });
    });

    test('rejects when skills dir cannot be resolved', async () => {
        const noDirService = new FileSystemSkillInjectionService({
            resolveSkillsDir: () => '',
        });
        await expect(noDirService.inject({ name: 'foo', content: 'body' }))
            .rejects.toMatchObject({ code: 'NO_SKILLS_DIR' });
    });

    test('creates skills dir if it does not exist', async () => {
        fs.rmSync(dir, { recursive: true, force: true });
        expect(fs.existsSync(dir)).toBe(false);
        const result = await service.inject({ name: 'foo', content: 'body' });
        expect(fs.existsSync(result.filePath)).toBe(true);
    });

    test('falls back to safeName when displayName is blank', async () => {
        const result = await service.inject({ name: 'bare', content: 'body' });
        const meta = JSON.parse(fs.readFileSync(result.metaPath, 'utf8'));
        expect(meta.displayName).toBe('bare');
        expect(meta.injectedFrom).toBe('external');
    });

    test('overwrites existing skill on repeated inject', async () => {
        await service.inject({ name: 'foo', content: 'v1' });
        const r2 = await service.inject({ name: 'foo', content: 'v2' });
        expect(fs.readFileSync(r2.filePath, 'utf8')).toBe('v2');
    });

    test('fires onInjected hook on success', async () => {
        const calls: any[] = [];
        const hooked = new FileSystemSkillInjectionService({
            resolveSkillsDir: () => dir,
            onInjected: (result, req) => calls.push({ result, req }),
        });
        await hooked.inject({ name: 'hooked', content: 'body', displayName: 'Hooked Skill' });
        expect(calls).toHaveLength(1);
        expect(calls[0].result.safeName).toBe('hooked');
        expect(calls[0].req.displayName).toBe('Hooked Skill');
    });

    test('SkillInjectionError is thrown for write failures', async () => {
        // Point at a path that exists but is not a directory — mkdirSync will
        // throw. The service should wrap it as WRITE_FAILED.
        const file = path.join(dir, 'notadir');
        fs.writeFileSync(file, 'x');
        const broken = new FileSystemSkillInjectionService({
            resolveSkillsDir: () => path.join(file, 'sub'),
        });
        await expect(broken.inject({ name: 'foo', content: 'body' }))
            .rejects.toMatchObject({ name: 'SkillInjectionError', code: 'WRITE_FAILED' });
    });
});
