import path from 'node:path';

import { describe, expect, it } from 'vitest';

import {
    buildTurboReleaseCommand,
    buildTurboReleaseTaskCommand,
    MAX_BUILD_OUTPUT_CHARS,
    PnpmBuildRunner,
    resolveReleaseTurboCacheDir,
    resolveReleaseTurboConcurrency,
} from '../src/orchestrator/buildRunner.js';

describe('Turbo release build plan', () => {
    it('anchors the default cache outside commit worktrees and allows an operator override', () => {
        expect(resolveReleaseTurboCacheDir('/srv/core/repository')).toBe('/srv/core/repository/.turbo/release-cache');
        expect(
            resolveReleaseTurboCacheDir('/srv/core/repository', {
                TURBO_CACHE_DIR: '/srv/core/cache/turbo',
            })
        ).toBe('/srv/core/cache/turbo');
        expect(
            resolveReleaseTurboCacheDir('/srv/core/repository', {
                TURBO_CACHE_DIR: '.cache/turbo',
            })
        ).toBe('/srv/core/repository/.cache/turbo');
    });

    it('defaults to one worker for bounded runtimes and accepts a larger-host override', () => {
        expect(resolveReleaseTurboConcurrency()).toBe(1);
        expect(resolveReleaseTurboConcurrency({ RELEASE_TURBO_CONCURRENCY: '2' })).toBe(2);
        expect(() => resolveReleaseTurboConcurrency({ RELEASE_TURBO_CONCURRENCY: '0' })).toThrow(
            'RELEASE_TURBO_CONCURRENCY must be a positive integer.'
        );
    });

    it('uses a bounded streaming Turbo build for the selected packages', () => {
        expect(
            buildTurboReleaseCommand(
                '/srv/core/profile-worktrees/commit',
                '/srv/core/repository',
                ['@sammo-ts/game-api'],
                { NODE_ENV: 'production' }
            )
        ).toEqual({
            command: 'pnpm',
            args: [
                'exec',
                'turbo',
                'run',
                'build',
                '--filter=@sammo-ts/game-api',
                '--cache-dir=/srv/core/repository/.turbo/release-cache',
                '--concurrency=1',
                '--ui=stream',
                '--output-logs=new-only',
            ],
            cwd: '/srv/core/profile-worktrees/commit',
            env: { NODE_ENV: 'production' },
        });
    });

    it('uses the same bounded cache policy for a release-specific task', () => {
        expect(
            buildTurboReleaseTaskCommand(
                '/srv/core/profile-worktrees/commit',
                '/srv/core/repository',
                'build:release',
                ['@sammo-ts/game-frontend'],
                { VITE_APP_BASE_PATH: '/che' }
            ).args
        ).toEqual([
            'exec',
            'turbo',
            'run',
            'build:release',
            '--filter=@sammo-ts/game-frontend',
            '--cache-dir=/srv/core/repository/.turbo/release-cache',
            '--concurrency=1',
            '--ui=stream',
            '--output-logs=new-only',
        ]);
    });
});

describe('PnpmBuildRunner', () => {
    it('returns a failed result when a command cannot be spawned', async () => {
        const runner = new PnpmBuildRunner();

        const result = await runner.run([
            {
                command: path.join(process.cwd(), 'missing-build-command'),
                args: [],
                cwd: process.cwd(),
            },
        ]);

        expect(result.ok).toBe(false);
        expect(result.exitCode).toBeNull();
        expect(result.output).toContain('ENOENT');
    });

    it('retains only a bounded tail across command output', async () => {
        const runner = new PnpmBuildRunner();
        const result = await runner.run([
            {
                command: process.execPath,
                args: ['-e', `process.stdout.write('a'.repeat(${MAX_BUILD_OUTPUT_CHARS}));`],
                cwd: process.cwd(),
            },
            {
                command: process.execPath,
                args: ['-e', "process.stdout.write('tail-marker');"],
                cwd: process.cwd(),
            },
        ]);

        expect(result.ok).toBe(true);
        expect(result.output.length).toBe(MAX_BUILD_OUTPUT_CHARS);
        expect(result.output.endsWith('tail-marker')).toBe(true);
    });

    it('streams command boundaries and line-buffered output to an observer', async () => {
        const runner = new PnpmBuildRunner();
        const events: Array<{ type: string; message?: string }> = [];

        const result = await runner.run(
            [
                {
                    command: process.execPath,
                    args: ['-e', "process.stdout.write('first\\npartial');"],
                    cwd: process.cwd(),
                },
            ],
            async (event) => {
                events.push({ type: event.type, ...('message' in event ? { message: event.message } : {}) });
            }
        );

        expect(result.ok).toBe(true);
        expect(events).toEqual([
            { type: 'COMMAND_START' },
            { type: 'OUTPUT', message: 'first' },
            { type: 'OUTPUT', message: 'partial' },
            { type: 'COMMAND_END' },
        ]);
    });

    it('terminates a running build process group when the operation is cancelled', async () => {
        const runner = new PnpmBuildRunner();
        const abortController = new AbortController();
        const startedAt = Date.now();
        const timer = setTimeout(() => abortController.abort(), 50);

        const result = await runner.run(
            [
                {
                    command: process.execPath,
                    args: ['-e', "setInterval(() => process.stdout.write('still-running\\n'), 25);"],
                    cwd: process.cwd(),
                },
            ],
            undefined,
            { signal: abortController.signal, terminateGraceMs: 100 }
        );
        clearTimeout(timer);

        expect(result).toMatchObject({ ok: false, aborted: true });
        expect(result.output).toContain('Build cancelled by operator.');
        expect(Date.now() - startedAt).toBeLessThan(2_000);
    });
});
