import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync, closeSync, openSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { GitClient } from '@main/services/GitClient.js';

const execFileAsync = promisify(execFile);

// Empty file used as GIT_CONFIG_GLOBAL/SYSTEM to isolate from the user's git config.
let emptyConfigPath: string;
let emptyConfigDir: string;

function gitEnv(): NodeJS.ProcessEnv {
  return {
    ...process.env,
    GIT_CONFIG_GLOBAL: emptyConfigPath,
    GIT_CONFIG_SYSTEM: emptyConfigPath,
    GIT_AUTHOR_NAME: 'Test',
    GIT_AUTHOR_EMAIL: 'test@inkling.local',
    GIT_COMMITTER_NAME: 'Test',
    GIT_COMMITTER_EMAIL: 'test@inkling.local'
  };
}

async function initRepo(dir: string) {
  const env = gitEnv();
  await execFileAsync('git', ['init', '-b', 'main', dir], { env });
  await execFileAsync('git', ['-C', dir, 'config', 'user.email', 'test@inkling.local'], { env });
  await execFileAsync('git', ['-C', dir, 'config', 'user.name', 'Test'], { env });
  // Initial commit so HEAD exists.
  writeFileSync(join(dir, 'init.txt'), 'init');
  await execFileAsync('git', ['-C', dir, 'add', '-A'], { env });
  await execFileAsync('git', ['-C', dir, 'commit', '-m', 'initial'], { env });
}

describe('GitClient', () => {
  let dir: string;
  let prevEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    // Create an empty file for GIT_CONFIG_GLOBAL/SYSTEM to neutralize the user's global git config.
    emptyConfigDir = mkdtempSync(join(tmpdir(), 'gc-cfg-'));
    emptyConfigPath = join(emptyConfigDir, 'empty.gitconfig');
    closeSync(openSync(emptyConfigPath, 'w')); // touch
    // Stash and replace process.env so child gits inherit isolated config (GitClient uses default env).
    prevEnv = { ...process.env };
    Object.assign(process.env, {
      GIT_CONFIG_GLOBAL: emptyConfigPath,
      GIT_CONFIG_SYSTEM: emptyConfigPath,
      GIT_AUTHOR_NAME: 'Test',
      GIT_AUTHOR_EMAIL: 'test@inkling.local',
      GIT_COMMITTER_NAME: 'Test',
      GIT_COMMITTER_EMAIL: 'test@inkling.local'
    });
    dir = mkdtempSync(join(tmpdir(), 'gc-'));
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
    rmSync(emptyConfigDir, { recursive: true, force: true });
    // Restore env exactly.
    for (const k of Object.keys(process.env)) delete process.env[k];
    Object.assign(process.env, prevEnv);
  });

  it('isRepo returns false when dir is not a repo', async () => {
    const g = new GitClient(dir);
    expect(await g.isRepo()).toBe(false);
  });

  it('isRepo returns true after git init', async () => {
    await initRepo(dir);
    const g = new GitClient(dir);
    expect(await g.isRepo()).toBe(true);
  });

  it('hasRemote returns false when no remote', async () => {
    await initRepo(dir);
    const g = new GitClient(dir);
    expect(await g.hasRemote()).toBe(false);
  });

  it('hasRemote returns true after remote add', async () => {
    await initRepo(dir);
    const remoteDir = mkdtempSync(join(tmpdir(), 'gc-remote-'));
    try {
      await execFileAsync('git', ['init', '--bare', remoteDir], { env: gitEnv() });
      await execFileAsync('git', ['-C', dir, 'remote', 'add', 'origin', remoteDir], { env: gitEnv() });
      const g = new GitClient(dir);
      expect(await g.hasRemote()).toBe(true);
    } finally {
      rmSync(remoteDir, { recursive: true, force: true });
    }
  });

  it('commit returns changed=false when nothing to commit', async () => {
    await initRepo(dir);
    const g = new GitClient(dir);
    const r = await g.commit('test');
    expect(r.changed).toBe(false);
    expect(r.sha).toBeNull();
  });

  it('commit returns changed=true with sha when something to commit', async () => {
    await initRepo(dir);
    writeFileSync(join(dir, 'new.txt'), 'hi');
    const g = new GitClient(dir);
    await g.addAll();
    const r = await g.commit('test commit');
    expect(r.changed).toBe(true);
    expect(r.sha).toMatch(/^[0-9a-f]{40}$/);
  });

  it('currentBranch returns the branch name', async () => {
    await initRepo(dir);
    const g = new GitClient(dir);
    const branch = await g.currentBranch();
    expect(branch).toBe('main');
  });

  it('run returns non-zero exit code without throwing for invalid args', async () => {
    await initRepo(dir);
    const g = new GitClient(dir);
    const r = await g.run(['this-is-not-a-real-git-subcommand']);
    expect(r.exitCode).not.toBe(0);
  });
});
