import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

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

import { GatewayOrchestrator } from '../src/orchestrator/gatewayOrchestrator.js';
import type { BuildCommand } from '../src/orchestrator/buildRunner.js';
import { FrontendArtifactManager } from '../src/orchestrator/frontendArtifactManager.js';
import type { ProcessManager } from '../src/orchestrator/processManager.js';
import type {
    GatewayClaimedProfileUpdate,
    GatewayOperationRecord,
    GatewayProfileRecord,
    GatewayProfileRepository,
} from '../src/orchestrator/profileRepository.js';
import type { GitWorkspaceManager } from '../src/orchestrator/workspaceManager.js';

const SHA = '1111111111111111111111111111111111111111';
const temporaryDirectories: string[] = [];

const createReleaseWorkspace = async (): Promise<string> => {
    const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-profile-deploy-'));
    temporaryDirectories.push(workspace);
    await fs.mkdir(path.join(workspace, 'packages/infra/prisma/gateway-migrations/20260801000000_gateway'), {
        recursive: true,
    });
    await fs.mkdir(path.join(workspace, 'packages/infra/prisma/migrations/20260801000000_game'), {
        recursive: true,
    });
    await fs.writeFile(
        path.join(workspace, 'release-manifest.json'),
        JSON.stringify({
            formatVersion: 1,
            controllerProtocol: 1,
            gatewaySchemaHead: '20260801000000_gateway',
            gameSchemaHead: '20260801000000_game',
            components: ['game-api', 'game-engine', 'game-frontend'],
        })
    );
    await fs.mkdir(path.join(workspace, 'app', 'game-frontend', '.release-build', 'assets'), {
        recursive: true,
    });
    await fs.writeFile(
        path.join(workspace, 'app', 'game-frontend', '.release-build', 'index.html'),
        '<!doctype html><title>static profile</title><script type="module" src="./assets/app-deadbeef.js"></script>'
    );
    await fs.writeFile(
        path.join(workspace, 'app', 'game-frontend', '.release-build', 'assets', 'app-deadbeef.js'),
        'console.log("static")'
    );
    await fs.writeFile(
        path.join(workspace, 'app', 'game-frontend', '.release-build', 'deployment-version.json'),
        `${JSON.stringify({ buildCommitSha: SHA })}\n`
    );
    return workspace;
};

afterEach(async () => {
    await Promise.all(temporaryDirectories.splice(0).map((directory) => fs.rm(directory, { recursive: true })));
});

describe('profile DEPLOY operation', () => {
    it('migrates and atomically switches a static frontend without executing the reset seed path', async () => {
        const workspace = await createReleaseWorkspace();
        const artifactRoot = path.join(workspace, 'artifact-volume');
        const oldFrontendRoot = path.join(workspace, 'old-static-frontend');
        await fs.mkdir(oldFrontendRoot, { recursive: true });
        await fs.writeFile(path.join(oldFrontendRoot, 'index.html'), '<!doctype html><title>old static profile</title>');
        const artifactManager = new FrontendArtifactManager(artifactRoot);
        const oldArtifact = await artifactManager.stage({
            frontendKey: 'che',
            sourceRoot: oldFrontendRoot,
            commitSha: '2222222222222222222222222222222222222222',
        });
        await artifactManager.activate('che', oldArtifact.releaseId);
        const profile: GatewayProfileRecord = {
            profileName: 'che:1010',
            profile: 'che',
            instanceKey: '1010',
            currentScenario: '1010',
            scenario: '1010',
            apiPort: 15003,
            status: 'RUNNING',
            buildStatus: 'SUCCEEDED',
            buildCommitSha: '2222222222222222222222222222222222222222',
            buildWorkspace: '/srv/sammo/old',
            meta: {},
            createdAt: '2026-08-01T00:00:00.000Z',
            updatedAt: '2026-08-01T00:00:00.000Z',
        };
        const operation: GatewayOperationRecord = {
            id: '33333333-3333-4333-8333-333333333333',
            profileName: profile.profileName,
            type: 'DEPLOY',
            status: 'RUNNING',
            sourceMode: 'COMMIT',
            sourceRef: SHA,
            payload: {},
            requestedBy: 'admin',
            createdAt: '2026-08-01T00:00:00.000Z',
            updatedAt: '2026-08-01T00:00:00.000Z',
        };
        let nextOperation: GatewayOperationRecord | null = operation;
        const patches: GatewayClaimedProfileUpdate[] = [];
        const completions: string[] = [];
        const logs: Array<{ phase: string; message: string; level: string }> = [];
        const repository: GatewayProfileRepository = {
            listProfiles: async () => [profile],
            getProfile: async () => profile,
            upsertProfile: async () => profile,
            updateCurrentScenario: async () => profile,
            updateStatus: async () => profile,
            updateBuildStatus: async () => profile,
            updateMeta: async () => profile,
            listReservedToStart: async () => [],
            findQueuedBuild: async () => null,
            updateLastError: async () => {},
            updateWorkspaceUsage: async () => {},
            clearWorkspaceUsage: async () => {},
            listOperations: async () => [],
            getOperation: async () => operation,
            listOperationLogs: async () => [],
            appendOperationLog: async (operationId, input) => {
                logs.push(input);
                return {
                    cursor: String(logs.length),
                    operationId,
                    createdAt: '2026-08-11T00:00:00.000Z',
                    ...input,
                };
            },
            createOperation: async () => operation,
            claimNextOperation: async () => {
                const value = nextOperation;
                nextOperation = null;
                return value;
            },
            renewOperationLease: async () => true,
            pinOperationResolvedCommit: async () => true,
            updateProfileForOperation: async (_id, _owner, _profileName, patch) => {
                patches.push(patch);
                return { ...profile, ...patch } as GatewayProfileRecord;
            },
            completeOperation: async (_id, status) => {
                completions.push(status);
                return { ...operation, status };
            },
            requeueOperation: async () => operation,
            cancelOperation: async () => false,
            retryOperation: async () => null,
        };
        const processNames = [
            'sammo:che:1010:game-frontend',
            'sammo:che:1010:game-api',
            'sammo:che:1010:turn-daemon',
            'sammo:che:1010:auction-worker',
            'sammo:che:1010:battle-sim-worker',
            'sammo:che:1010:tournament-worker',
        ];
        const backendProcessNames = processNames.filter((name) => !name.endsWith(':game-frontend'));
        const running = new Set(processNames);
        const startedDefinitions: Array<Parameters<ProcessManager['start']>[0]> = [];
        const frontendDuringStop: string[] = [];
        const processManager: ProcessManager = {
            list: async () => [...running].map((name) => ({ name, status: 'online' })),
            start: async (definition) => {
                startedDefinitions.push(definition);
                running.add(definition.name);
            },
            stop: async () => {
                frontendDuringStop.push(await fs.readFile(path.join(artifactRoot, 'che', 'current', 'index.html'), 'utf8'));
            },
            delete: async (name) => {
                running.delete(name);
            },
        };
        const commandGroups: BuildCommand[][] = [];
        const workspaceManager = {
            resolveCommit: async () => SHA,
            prepare: async () => ({ root: workspace, created: true, needsInstall: true }),
        } as unknown as GitWorkspaceManager;
        const orchestrator = new GatewayOrchestrator({
            repository,
            processManager,
            buildRunner: {
                run: async (commands, onProgress) => {
                    commandGroups.push(commands);
                    for (const command of commands) {
                        await onProgress?.({ type: 'COMMAND_START', command });
                        await onProgress?.({
                            type: 'OUTPUT',
                            stream: 'stdout',
                            message: 'built profile with postgresql://user:pass@integration.invalid/sammo',
                        });
                        await onProgress?.({ type: 'COMMAND_END', command, exitCode: 0 });
                    }
                    return { ok: true, exitCode: 0, output: '' };
                },
            },
            workspaceManager,
            processConfig: {
                workspaceRoot: '/srv/sammo/controller',
                redisKeyPrefix: 'sammo:test',
                gameTokenSecret: 'test-secret',
                gatewayInternalApiUrl: 'http://127.0.0.1:15001',
                frontendServeMode: 'static',
                frontendArtifactRoot: artifactRoot,
                frontendReadinessOrigin: 'http://caddy',
                baseEnv: {
                    GATEWAY_DATABASE_URL:
                        'postgresql://user:encoded%23password@integration.invalid/sammo?schema=public',
                    POSTGRES_USER: 'user',
                    POSTGRES_PASSWORD: 'raw#password',
                    POSTGRES_HOST: 'integration.invalid',
                    POSTGRES_PORT: '5432',
                    POSTGRES_DB: 'sammo',
                },
            },
            reconcileIntervalMs: 60_000,
            scheduleIntervalMs: 60_000,
            buildIntervalMs: 60_000,
            adminActionIntervalMs: 60_000,
            profileReadinessTimeoutMs: 10,
            fetchImpl: async () => new Response('', { status: 200 }),
        });

        await orchestrator.runOperationsNow();

        expect(commandGroups).toHaveLength(2);
        expect(commandGroups[0]?.[0]?.args).toEqual(['install', '--frozen-lockfile']);
        expect(commandGroups[0]?.[1]?.args).toContain('--filter=@sammo-ts/game-api');
        expect(commandGroups[0]?.[1]?.args).not.toContain('--filter=@sammo-ts/gateway-api');
        expect(commandGroups[0]?.[2]?.args).toContain('build:release');
        expect(commandGroups[0]?.[2]?.args).toContain('--cache-dir=/srv/sammo/controller/.turbo/release-cache');
        expect(commandGroups[0]?.[2]?.args).toContain('--concurrency=1');
        expect(commandGroups[0]?.[2]?.env?.VITE_BUILD_COMMIT_SHA).toBe(SHA);
        expect(commandGroups[0]?.[2]?.env?.VITE_ASSET_BASE_PATH).toBe('./');
        expect(commandGroups[0]?.[2]?.env).not.toHaveProperty('VITE_APP_BASE_PATH');
        expect(commandGroups[0]?.[2]?.env).not.toHaveProperty('VITE_GAME_API_URL');
        expect(commandGroups[0]?.[2]?.env).not.toHaveProperty('VITE_GAME_SSE_URL');
        expect(commandGroups[0]?.[2]?.env).not.toHaveProperty('VITE_GAME_PROFILE');
        expect(commandGroups[1]?.[0]?.args.slice(0, 6)).toEqual([
            '--filter',
            '@sammo-ts/infra',
            'exec',
            'node',
            '--input-type=module',
            '--eval',
        ]);
        expect(commandGroups[1]?.[0]?.args.at(-1)).toContain("current_setting('TimeZone')");
        expect(commandGroups[1]?.[1]?.args).toEqual([
            '--filter',
            '@sammo-ts/infra',
            'prisma:migrate:deploy:game',
        ]);
        expect(commandGroups[1]?.[0]?.env?.DATABASE_URL).toBe(
            'postgresql://user:encoded%23password@integration.invalid/sammo?schema=che'
        );
        expect(commandGroups[1]?.[1]?.env?.DATABASE_URL).toBe(
            'postgresql://user:encoded%23password@integration.invalid/sammo?schema=che&options=-c+TimeZone%3DAsia%2FSeoul'
        );
        expect(startedDefinitions).toHaveLength(backendProcessNames.length);
        for (const definition of startedDefinitions) {
            expect(definition.env?.DATABASE_URL).toBe(
                'postgresql://user:encoded%23password@integration.invalid/sammo?schema=che'
            );
        }
        expect(commandGroups.flat().some((command) => command.env?.GATEWAY_ROLE === 'profile-seed')).toBe(false);
        expect(patches.at(-1)).toMatchObject({
            buildStatus: 'SUCCEEDED',
            buildCommitSha: SHA,
            buildWorkspace: workspace,
            meta: { releaseSource: { mode: 'COMMIT', ref: SHA } },
        });
        expect(completions).toEqual(['SUCCEEDED']);
        expect(logs).toEqual(
            expect.arrayContaining([
                expect.objectContaining({ phase: 'resolve', message: `대상 커밋을 ${SHA}로 고정했습니다.` }),
                expect.objectContaining({ phase: 'build', level: 'OUTPUT' }),
                expect.objectContaining({ phase: 'migration', level: 'OUTPUT' }),
                expect.objectContaining({ phase: 'readiness', message: 'profile readiness 확인을 통과했습니다.' }),
                expect.objectContaining({ phase: 'complete', message: 'DB 보존 버전 업데이트가 완료되었습니다.' }),
            ])
        );
        expect(logs.map((entry) => entry.message).join('\n')).not.toContain('pass@integration.invalid');
        expect(logs.map((entry) => entry.message).join('\n')).toContain('[REDACTED]');
        expect([...running].sort()).toEqual([...backendProcessNames].sort());
        expect(frontendDuringStop).toHaveLength(processNames.length);
        expect(frontendDuringStop.every((html) => html.includes('old static profile'))).toBe(true);
        expect(
            await fs.readFile(path.join(artifactRoot, 'che', 'current', 'index.html'), 'utf8')
        ).toContain('/gateway/profile-assets/');
    });
});
