import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import { tmpdir } 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 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/claude-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: 'claude-sonnet-4', reasoningEffort: 'medium' },
  aiProviderProfile: {
    id: 'p',
    name: 'Anthropic',
    provider: 'anthropic',
    authType: 'api_key',
    secret: 'sk-ant-test',
    model: 'claude-sonnet-4',
    reasoningEffort: 'medium',
  },
  github: {},
  secrets: [],
} as unknown as Claim;

const makeWorkspace = (): AdapterWorkspace => ({
  claim,
  workdir: '/tmp/spoon-claude-adapter-test-missing',
  homeDir: '/tmp/spoon-claude-adapter-test-missing',
  username: 'tester',
  containerHome: '/home/tester',
  containerRepo: '/home/tester/Code/spoon',
  repoDir: '/tmp/spoon-claude-adapter-test-missing/Code/spoon',
  boxName: 'spoon-box-tester',
  redact: (value: string) => value,
  runtime: 'claude',
});

const makeOAuthWorkspace = (
  homeDir: string,
  authType: 'anthropic_oauth_json' | 'opencode_auth_json',
): AdapterWorkspace => ({
  claim: {
    ...claim,
    aiProviderProfile: {
      id: 'p',
      name: 'Anthropic',
      provider: 'anthropic',
      authType,
      secret: JSON.stringify({ access_token: 'oauth-token' }),
      model: 'claude-sonnet-4',
      reasoningEffort: 'medium',
    },
  } as unknown as Claim,
  workdir: homeDir,
  homeDir,
  username: 'tester',
  containerHome: '/home/tester',
  containerRepo: '/home/tester/Code/spoon',
  repoDir: path.join(homeDir, 'Code/spoon'),
  boxName: 'spoon-box-tester',
  redact: (value: string) => value,
  runtime: 'claude',
});

const tempDirs: string[] = [];

afterEach(async () => {
  while (tempDirs.length) {
    const dir = tempDirs.pop();
    if (dir) await rm(dir, { recursive: true, force: true });
  }
});

describe('ClaudeCodeAdapter prepareAuth', () => {
  test('writes credentials.json for an anthropic_oauth_json profile', async () => {
    const { createClaudeAdapter } = await loadAdapter();
    const homeDir = await mkdtemp(path.join(tmpdir(), 'spoon-claude-oauth-'));
    tempDirs.push(homeDir);

    const adapter = createClaudeAdapter();
    await adapter.prepareAuth(makeOAuthWorkspace(homeDir, 'anthropic_oauth_json'));

    const credentialsPath = path.join(homeDir, '.claude', '.credentials.json');
    const contents = await readFile(credentialsPath, 'utf8');
    expect(contents).toContain('oauth-token');
  });

  test('does not write credentials.json for an opencode_auth_json profile', async () => {
    const { createClaudeAdapter } = await loadAdapter();
    const homeDir = await mkdtemp(path.join(tmpdir(), 'spoon-claude-oauth-'));
    tempDirs.push(homeDir);

    const adapter = createClaudeAdapter();
    await adapter.prepareAuth(makeOAuthWorkspace(homeDir, 'opencode_auth_json'));

    const credentialsPath = path.join(homeDir, '.claude', '.credentials.json');
    await expect(stat(credentialsPath)).rejects.toThrow();
  });
});

describe('ClaudeCodeAdapter', () => {
  test('streams normalized events and marks the turn via buildMarkedCommand', async () => {
    const { createClaudeAdapter } = await loadAdapter();
    const captured: unknown[] = [];
    const execStream = vi.fn(async (args) => {
      captured.push(args.command);
      await args.onStdoutLine?.(
        JSON.stringify({
          type: 'system',
          subtype: 'init',
          session_id: 'abc',
        }),
      );
      await args.onStdoutLine?.(
        JSON.stringify({
          type: 'assistant',
          message: { content: [{ type: 'text', text: 'hi there' }] },
        }),
      );
      await args.onStdoutLine?.(
        JSON.stringify({
          type: 'result',
          subtype: 'success',
          result: 'final text',
          session_id: 'abc',
        }),
      );
      return { exitCode: 0, output: '' };
    }) as unknown as ExecStreamFn;

    const adapter = createClaudeAdapter({ 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: 'session', sessionId: 'abc' });
    expect(events).toContainEqual({
      kind: 'assistant_delta',
      content: 'hi there',
    });
    expect(result.finalMessage).toBe('final text');
    expect(result.sessionId).toBe('abc');
    expect(result.error).toBeUndefined();

    // The turn ran through buildMarkedCommand (setsid bash session) carrying the
    // `claude -p` argv in the script.
    const command = captured[0] as string[];
    expect(command[0]).toBe('setsid');
    expect(command[1]).toBe('bash');
    expect(command[3]).toContain("'claude' '-p'");
    expect(command[3]).toContain("'--output-format' 'stream-json'");
  });
});
