import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { afterEach, describe, expect, test, vi } from 'vitest';

import type { NormalizedAgentEvent } from '../../src/agent-events';
import type { ExecStreamFn } from '../../src/runtime/agent-runtime';
import type { AdapterWorkspace, Claim } from '../../src/runtime/provider';

const tempDirs: string[] = [];

const pathExists = async (filePath: string): Promise<boolean> => {
  try {
    await stat(filePath);
    return true;
  } catch {
    return false;
  }
};

const loadAdapter = async () => {
  process.env.SPOON_WORKER_TOKEN = 'test-worker-token';
  process.env.GITHUB_APP_ID = '123';
  process.env.GITHUB_APP_PRIVATE_KEY =
    '-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----';
  return await import('../../src/runtime/codex-adapter');
};

const claim = {
  job: {
    _id: 'job-1',
    prompt: 'hi',
    baseBranch: 'main',
    workBranch: 'spoon/x',
    forkOwner: 'o',
    forkRepo: 'r',
    upstreamOwner: 'u',
    upstreamRepo: 'r',
  },
  spoon: { name: 'Spoon' },
  openai: { model: 'gpt-5', reasoningEffort: 'medium' },
  aiProviderProfile: {
    id: 'p',
    name: 'Codex',
    provider: 'opencode_openai_login',
    authType: 'opencode_auth_json',
    model: 'gpt-5',
    reasoningEffort: 'medium',
  },
  github: {},
  secrets: [],
} as unknown as Claim;

const makeWorkspace = (): AdapterWorkspace => ({
  claim,
  // A non-existent workdir so the --output-last-message read is a clean ENOENT.
  workdir: '/tmp/spoon-codex-adapter-test-missing',
  homeDir: '/tmp/spoon-codex-adapter-test-missing',
  username: 'tester',
  containerHome: '/home/tester',
  containerRepo: '/home/tester/Code/spoon',
  repoDir: '/tmp/spoon-codex-adapter-test-missing/Code/spoon',
  boxName: 'spoon-box-tester',
  redact: (value: string) => value,
  runtime: 'codex',
});

const makeAuthWorkspace = async (
  profile: Record<string, unknown>,
): Promise<AdapterWorkspace> => {
  const workdir = await mkdtemp(path.join(os.tmpdir(), 'spoon-codex-auth-'));
  tempDirs.push(workdir);
  const repoDir = path.join(workdir, 'Code', 'spoon');
  await mkdir(repoDir, { recursive: true });
  return {
    ...makeWorkspace(),
    claim: { ...claim, aiProviderProfile: profile } as unknown as Claim,
    workdir,
    homeDir: workdir,
    repoDir,
  };
};

describe('CodexAdapter', () => {
  afterEach(async () => {
    await Promise.all(
      tempDirs.map((dir) => rm(dir, { force: true, recursive: true })),
    );
    tempDirs.length = 0;
  });

  test('prepareAuth skips auth.json for an api_key profile', async () => {
    const { createCodexAdapter } = await loadAdapter();
    const adapter = createCodexAdapter();
    const workspace = await makeAuthWorkspace({
      id: 'p',
      name: 'OpenAI',
      provider: 'openai',
      authType: 'api_key',
      model: 'gpt-5',
      // A raw API key, which is deliberately NOT valid JSON.
      secret: 'sk-test',
    });

    await expect(adapter.prepareAuth?.(workspace)).resolves.toBeUndefined();

    const codexAuthPath = path.join(workspace.workdir, '.codex', 'auth.json');
    const openCodeAuthPath = path.join(
      workspace.workdir,
      '.local',
      'share',
      'opencode',
      'auth.json',
    );
    await expect(pathExists(codexAuthPath)).resolves.toBe(false);
    await expect(pathExists(openCodeAuthPath)).resolves.toBe(false);
  });

  test('prepareAuth writes auth.json for a ChatGPT-login profile', async () => {
    const { createCodexAdapter } = await loadAdapter();
    const adapter = createCodexAdapter();
    const secret = JSON.stringify({ tokens: { access: 'abc' } });
    const workspace = await makeAuthWorkspace({
      id: 'p',
      name: 'Codex',
      provider: 'opencode_openai_login',
      authType: 'opencode_auth_json',
      model: 'gpt-5',
      secret,
    });

    await adapter.prepareAuth?.(workspace);

    const codexAuthPath = path.join(workspace.workdir, '.codex', 'auth.json');
    await expect(readFile(codexAuthPath, 'utf8')).resolves.toBe(
      `${JSON.stringify(JSON.parse(secret), null, 2)}\n`,
    );
    const openCodeAuthPath = path.join(
      workspace.workdir,
      '.local',
      'share',
      'opencode',
      'auth.json',
    );
    await expect(readFile(openCodeAuthPath, 'utf8')).resolves.toBe(
      `${JSON.stringify(JSON.parse(secret), null, 2)}\n`,
    );
  });

  test('streams normalized events and marks the turn via buildMarkedCommand', async () => {
    const { createCodexAdapter } = await loadAdapter();
    const captured: unknown[] = [];
    const execStream = vi.fn(async (args) => {
      captured.push(args.command);
      await args.onStdoutLine?.(
        JSON.stringify({
          type: 'item.completed',
          item: { id: 'item-1', type: 'agent_message', text: 'done' },
        }),
      );
      await args.onStdoutLine?.(JSON.stringify({ type: 'turn.completed' }));
      return { exitCode: 0, output: '' };
    }) as unknown as ExecStreamFn;

    const adapter = createCodexAdapter({ execStream });
    const events: NormalizedAgentEvent[] = [];
    const onEvent = (event: NormalizedAgentEvent): Promise<void> => {
      events.push(event);
      return Promise.resolve();
    };

    const result = await adapter.runTurn(makeWorkspace(), 'do it', onEvent);

    expect(events).toContainEqual({
      kind: 'assistant_delta',
      content: 'done\n\n',
      externalMessageId: 'item-1',
    });
    expect(events).toContainEqual({ kind: 'assistant_completed' });
    expect(result).toBeTypeOf('object');
    // The turn ran through buildMarkedCommand (setsid bash session).
    const command = captured[0] as string[];
    expect(command[0]).toBe('setsid');
    expect(command[1]).toBe('bash');
  });

  test('surfaces a turn.failed error while exiting zero', async () => {
    const { createCodexAdapter } = await loadAdapter();
    const execStream = vi.fn(async (args) => {
      await args.onStdoutLine?.(
        JSON.stringify({
          type: 'turn.failed',
          error: { message: 'boom' },
        }),
      );
      return { exitCode: 0, output: '' };
    }) as unknown as ExecStreamFn;

    const adapter = createCodexAdapter({ execStream });
    const result = await adapter.runTurn(makeWorkspace(), 'do it', () =>
      Promise.resolve(),
    );

    expect(result.error).toContain('boom');
  });
});
