import { createHash } from 'crypto';
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
import os from 'os';
import path from 'path';
import { gzipSync } from 'zlib';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const execCliMock = vi.hoisted(() => vi.fn());

vi.mock('@main/utils/childProcess', () => ({
  execCli: execCliMock,
}));

import {
  extractCodexRuntimePackageFilesFromTarball,
  getCodexRuntimePlatformCandidates,
  resolveAppManagedCodexRuntimeBinaryPath,
  resolveVerifiedAppManagedCodexRuntimeBinaryPath,
  verifyCodexRuntimePackageIntegrity,
} from '@features/codex-runtime-installer/main';
import { setAppDataBasePath } from '@main/utils/pathDecoder';

let tempRoot: string | null = null;

function writeOctal(header: Buffer, offset: number, length: number, value: number): void {
  const encoded = value
    .toString(8)
    .padStart(length - 1, '0')
    .slice(-(length - 1));
  header.write(`${encoded}\0`, offset, length, 'ascii');
}

function createTarEntry(name: string, data: Buffer): Buffer {
  const header = Buffer.alloc(512);
  header.write(name, 0, Math.min(Buffer.byteLength(name), 100), 'utf8');
  writeOctal(header, 100, 8, 0o755);
  writeOctal(header, 108, 8, 0);
  writeOctal(header, 116, 8, 0);
  writeOctal(header, 124, 12, data.length);
  writeOctal(header, 136, 12, 0);
  header.fill(' ', 148, 156);
  header.write('0', 156, 1, 'ascii');
  header.write('ustar\0', 257, 6, 'ascii');
  header.write('00', 263, 2, 'ascii');
  const checksum = header.reduce((sum, byte) => sum + byte, 0);
  const checksumText = checksum.toString(8).padStart(6, '0');
  header.write(`${checksumText}\0 `, 148, 8, 'ascii');

  const padding = Buffer.alloc((512 - (data.length % 512)) % 512);
  return Buffer.concat([header, data, padding]);
}

function createTarball(entries: { name: string; data: string }[]): Buffer {
  return gzipSync(
    Buffer.concat([
      ...entries.map((entry) => createTarEntry(entry.name, Buffer.from(entry.data))),
      Buffer.alloc(1024),
    ])
  );
}

describe('CodexRuntimeInstallerService resolver', () => {
  beforeEach(async () => {
    tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-runtime-resolver-'));
    setAppDataBasePath(tempRoot);
    execCliMock.mockReset();
    execCliMock.mockResolvedValue({ stdout: 'codex-cli 1.0.0\n', stderr: '' });
  });

  afterEach(async () => {
    setAppDataBasePath(null);
    if (tempRoot) {
      await rm(tempRoot, { recursive: true, force: true });
      tempRoot = null;
    }
  });

  it('returns the current app-managed Codex binary path only when manifest and binary exist', async () => {
    const binaryPath = path.join(
      tempRoot!,
      'data',
      'runtimes',
      'codex',
      'versions',
      '1.0.0-darwin-arm64',
      'aarch64-apple-darwin',
      'codex',
      'codex'
    );
    const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'codex', 'current.json');
    await mkdir(path.dirname(binaryPath), { recursive: true });
    await mkdir(path.dirname(manifestPath), { recursive: true });
    await writeFile(binaryPath, 'binary', { mode: 0o755 });
    await writeFile(
      manifestPath,
      `${JSON.stringify({
        schemaVersion: 1,
        rootVersion: '1.0.0',
        platformVersion: '1.0.0-darwin-arm64',
        platformTarget: 'aarch64-apple-darwin',
        binaryPath,
        integrity: 'sha512-test',
        installedAt: '2026-05-13T00:00:00.000Z',
      })}\n`,
      'utf8'
    );

    expect(resolveAppManagedCodexRuntimeBinaryPath()).toBe(binaryPath);
  });

  it('ignores a manifest whose binary path is missing', async () => {
    const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'codex', 'current.json');
    await mkdir(path.dirname(manifestPath), { recursive: true });
    await writeFile(
      manifestPath,
      `${JSON.stringify({
        schemaVersion: 1,
        rootVersion: '1.0.0',
        platformVersion: '1.0.0-darwin-arm64',
        platformTarget: 'aarch64-apple-darwin',
        binaryPath: path.join(tempRoot!, 'missing-codex'),
        integrity: 'sha512-test',
        installedAt: '2026-05-13T00:00:00.000Z',
      })}\n`,
      'utf8'
    );

    expect(resolveAppManagedCodexRuntimeBinaryPath()).toBeNull();
  });

  it('returns the verified app-managed binary path only when --version succeeds', async () => {
    const binaryPath = path.join(
      tempRoot!,
      'data',
      'runtimes',
      'codex',
      'versions',
      '1.0.0-darwin-arm64',
      'aarch64-apple-darwin',
      'codex',
      'codex'
    );
    const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'codex', 'current.json');
    await mkdir(path.dirname(binaryPath), { recursive: true });
    await mkdir(path.dirname(manifestPath), { recursive: true });
    await writeFile(binaryPath, 'binary', { mode: 0o755 });
    await writeFile(
      manifestPath,
      `${JSON.stringify({
        schemaVersion: 1,
        rootVersion: '1.0.0',
        platformVersion: '1.0.0-darwin-arm64',
        platformTarget: 'aarch64-apple-darwin',
        binaryPath,
        integrity: 'sha512-test',
        installedAt: '2026-05-13T00:00:00.000Z',
      })}\n`,
      'utf8'
    );

    await expect(resolveVerifiedAppManagedCodexRuntimeBinaryPath()).resolves.toBe(binaryPath);
    expect(execCliMock).toHaveBeenCalledWith(binaryPath, ['--version'], {
      timeout: 10_000,
      windowsHide: true,
    });

    execCliMock.mockRejectedValueOnce(new Error('broken binary'));

    await expect(resolveVerifiedAppManagedCodexRuntimeBinaryPath()).resolves.toBeNull();
  });
});

describe('CodexRuntimeInstallerService package safety helpers', () => {
  it('selects expected platform packages', () => {
    expect(
      getCodexRuntimePlatformCandidates('darwin', 'arm64').map(
        (item) => item.optionalDependencyName
      )
    ).toEqual(['@openai/codex-darwin-arm64']);
    expect(
      getCodexRuntimePlatformCandidates('darwin', 'x64').map((item) => item.vendorTarget)
    ).toEqual(['x86_64-apple-darwin']);
    expect(
      getCodexRuntimePlatformCandidates('linux', 'x64').map((item) => item.vendorTarget)
    ).toEqual(['x86_64-unknown-linux-musl']);
    expect(
      getCodexRuntimePlatformCandidates('linux', 'arm64').map((item) => item.vendorTarget)
    ).toEqual(['aarch64-unknown-linux-musl']);
    expect(
      getCodexRuntimePlatformCandidates('win32', 'x64').map((item) => item.vendorTarget)
    ).toEqual(['x86_64-pc-windows-msvc']);
    expect(
      getCodexRuntimePlatformCandidates('win32', 'arm64').map((item) => item.vendorTarget)
    ).toEqual(['aarch64-pc-windows-msvc']);
  });

  it('fails npm integrity mismatches', () => {
    const payload = Buffer.from('actual package');
    const wrongHash = createHash('sha512').update('different package').digest('base64');

    expect(() => verifyCodexRuntimePackageIntegrity(payload, `sha512-${wrongHash}`)).toThrow(
      'integrity check failed'
    );
  });

  it('extracts the full selected Codex vendor payload from the package tarball', () => {
    const tarball = createTarball([
      { name: 'package/vendor/other-target/codex/codex', data: 'wrong' },
      { name: 'package/vendor/aarch64-apple-darwin/codex/codex', data: 'codex-binary' },
      { name: 'package/vendor/aarch64-apple-darwin/path/rg', data: 'rg-binary' },
    ]);

    const files = extractCodexRuntimePackageFilesFromTarball(
      tarball,
      'aarch64-apple-darwin',
      'codex'
    );

    expect(files.map((file) => file.relativePath).sort((a, b) => a.localeCompare(b))).toEqual([
      'codex/codex',
      'path/rg',
    ]);
    expect(files.find((file) => file.relativePath === 'codex/codex')?.data.toString()).toBe(
      'codex-binary'
    );
  });

  it('rejects tar path traversal before extraction', () => {
    const tarball = createTarball([
      { name: '../codex', data: 'unsafe' },
      { name: 'package/vendor/aarch64-apple-darwin/codex/codex', data: 'right' },
    ]);

    expect(() =>
      extractCodexRuntimePackageFilesFromTarball(tarball, 'aarch64-apple-darwin', 'codex')
    ).toThrow('Unsafe Codex package tar entry');
  });
});
