import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import {
  ensureBranchExists,
  mergeBranchToRelease,
  mergeReleaseToMain,
  syncMainProjectBranchForReservedRestart,
  type GitAutomationConfig,
} from './git-service.js';

const execFileAsync = promisify(execFile);

async function runGit(repoPath: string, args: string[]) {
  const { stdout } = await execFileAsync('git', ['-c', `safe.directory=${repoPath}`, '-C', repoPath, ...args], {
    encoding: 'utf8',
    env: {
      ...process.env,
      GIT_AUTHOR_NAME: 'test',
      GIT_AUTHOR_EMAIL: 'test@example.com',
      GIT_COMMITTER_NAME: 'test',
      GIT_COMMITTER_EMAIL: 'test@example.com',
    },
  });

  return stdout.trim();
}

async function createRepo() {
  const rootPath = await mkdtemp(path.join(tmpdir(), 'git-service-test-'));
  const remotePath = path.join(rootPath, 'remote.git');
  const repoPath = path.join(rootPath, 'work');
  const config: GitAutomationConfig = {
    repoPath,
    mainBranch: 'main',
    releaseBranch: 'release',
  };

  await execFileAsync('git', ['init', '--bare', '--initial-branch=main', remotePath], {
    encoding: 'utf8',
  });
  await execFileAsync('git', ['clone', remotePath, repoPath], {
    encoding: 'utf8',
    env: {
      ...process.env,
      GIT_TERMINAL_PROMPT: '0',
    },
  });

  await runGit(repoPath, ['config', 'user.name', 'test']);
  await runGit(repoPath, ['config', 'user.email', 'test@example.com']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'main root']);
  await runGit(repoPath, ['push', '-u', 'origin', 'main']);

  await runGit(repoPath, ['switch', '-c', 'release']);
  await runGit(repoPath, ['push', '-u', 'origin', 'release']);
  await runGit(repoPath, ['switch', 'main']);

  return { repoPath, config };
}

test('ensureBranchExists creates feature branch from main even when release diverged', async () => {
  const { repoPath, config } = await createRepo();

  await runGit(repoPath, ['switch', 'main']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'main base']);
  await runGit(repoPath, ['push', 'origin', 'main']);
  const mainHead = await runGit(repoPath, ['rev-parse', 'HEAD']);

  await runGit(repoPath, ['switch', 'release']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'release only']);
  await runGit(repoPath, ['push', 'origin', 'release']);
  const releaseHead = await runGit(repoPath, ['rev-parse', 'HEAD']);

  await ensureBranchExists(config, 'feature/test-branch', 'release');

  const featureHead = await runGit(repoPath, ['rev-parse', 'feature/test-branch']);

  assert.equal(featureHead, mainHead);
  assert.notEqual(featureHead, releaseHead, 'feature branch should point to main head, not release head');
});

test('ensureBranchExists recreates missing local main from origin/main', async () => {
  const { repoPath, config } = await createRepo();

  await runGit(repoPath, ['switch', 'main']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'main base']);
  await runGit(repoPath, ['push', 'origin', 'main']);
  const mainHead = await runGit(repoPath, ['rev-parse', 'main']);

  await runGit(repoPath, ['branch', '-D', 'main']);

  await ensureBranchExists(config, 'feature/recreated-from-origin-main');

  const recreatedMainHead = await runGit(repoPath, ['rev-parse', 'main']);
  const featureHead = await runGit(repoPath, ['rev-parse', 'feature/recreated-from-origin-main']);

  assert.equal(recreatedMainHead, mainHead);
  assert.equal(featureHead, mainHead);
});

test('mergeBranchToRelease squashes hotfix changes into release', async () => {
  const { repoPath, config } = await createRepo();

  await runGit(repoPath, ['switch', 'main']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'main base']);
  await runGit(repoPath, ['push', 'origin', 'main']);

  await runGit(repoPath, ['switch', 'release']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'release base']);
  await runGit(repoPath, ['push', 'origin', 'release']);

  await runGit(repoPath, ['switch', '-c', 'hotfix/test']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'hotfix change']);
  await runGit(repoPath, ['push', '-u', 'origin', 'hotfix/test']);
  const hotfixHead = await runGit(repoPath, ['rev-parse', 'HEAD']);

  await mergeBranchToRelease(config, 'hotfix/test', 'release');

  const releaseMessage = await runGit(repoPath, ['log', '-1', '--pretty=%s', 'release']);
  const parentCount = await runGit(repoPath, ['rev-list', '--parents', '-n', '1', 'release']);
  const releaseHistory = await runGit(repoPath, ['rev-list', 'release']);

  assert.equal(releaseMessage, 'merge: hotfix/test -> release (squash)');
  assert.equal(parentCount.split(' ').length, 2);
  assert.ok(!releaseHistory.split('\n').includes(hotfixHead));
});

test('mergeReleaseToMain keeps release to main as a normal merge commit', async () => {
  const { repoPath, config } = await createRepo();

  await runGit(repoPath, ['switch', 'main']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'main base']);
  await runGit(repoPath, ['push', 'origin', 'main']);

  await runGit(repoPath, ['switch', 'release']);
  await runGit(repoPath, ['commit', '--allow-empty', '-m', 'release change']);
  await runGit(repoPath, ['push', 'origin', 'release']);

  await mergeReleaseToMain(config, 'release');

  const mainMessage = await runGit(repoPath, ['log', '-1', '--pretty=%s', 'main']);
  const parentCount = await runGit(repoPath, ['rev-list', '--parents', '-n', '1', 'main']);

  assert.equal(mainMessage, 'merge: release -> main');
  assert.equal(parentCount.split(' ').length, 3);
});

test('syncMainProjectBranchForReservedRestart commits local changes and pushes them to origin main', async () => {
  const { repoPath } = await createRepo();
  const previousLocalMainMode = process.env.PLAN_LOCAL_MAIN_MODE;
  process.env.PLAN_LOCAL_MAIN_MODE = 'false';

  try {
    await runGit(repoPath, ['switch', 'main']);
    await writeFile(path.join(repoPath, 'note.txt'), 'hello reserved restart\n', 'utf8');

    const result = await syncMainProjectBranchForReservedRestart(
      repoPath,
      'main',
      'chore: sync main before reserved restart',
    );

    const head = await runGit(repoPath, ['rev-parse', 'HEAD']);
    const remoteHead = await runGit(repoPath, ['rev-parse', 'origin/main']);
    const mainMessage = await runGit(repoPath, ['log', '-1', '--pretty=%s', 'main']);
    const noteContent = await runGit(repoPath, ['show', 'HEAD:note.txt']);

    assert.equal(result.committed, true);
    assert.equal(result.commitMessage, 'chore: sync main before reserved restart');
    assert.equal(result.head, head);
    assert.equal(result.syncMode, 'remote');
    assert.equal(remoteHead, head);
    assert.equal(mainMessage, 'chore: sync main before reserved restart');
    assert.equal(noteContent, 'hello reserved restart');
  } finally {
    if (previousLocalMainMode === undefined) {
      delete process.env.PLAN_LOCAL_MAIN_MODE;
    } else {
      process.env.PLAN_LOCAL_MAIN_MODE = previousLocalMainMode;
    }
  }
});

test('syncMainProjectBranchForReservedRestart skips git sync when local main mode is enabled', async () => {
  const { repoPath } = await createRepo();
  const previousLocalMainMode = process.env.PLAN_LOCAL_MAIN_MODE;
  process.env.PLAN_LOCAL_MAIN_MODE = 'true';

  try {
    await runGit(repoPath, ['switch', 'main']);
    await writeFile(path.join(repoPath, 'note.txt'), 'hello local reserved restart\n', 'utf8');

    const headBefore = await runGit(repoPath, ['rev-parse', 'HEAD']);
    const remoteHeadBefore = await runGit(repoPath, ['rev-parse', 'origin/main']);
    const statusBefore = await runGit(repoPath, ['status', '--porcelain']);
    const result = await syncMainProjectBranchForReservedRestart(
      repoPath,
      'main',
      'chore: sync main before reserved restart',
    );
    const headAfter = await runGit(repoPath, ['rev-parse', 'HEAD']);
    const remoteHeadAfter = await runGit(repoPath, ['rev-parse', 'origin/main']);
    const statusAfter = await runGit(repoPath, ['status', '--porcelain']);

    assert.equal(result.committed, false);
    assert.equal(result.commitMessage, null);
    assert.equal(result.head, null);
    assert.equal(result.syncMode, 'local');
    assert.equal(remoteHeadAfter, remoteHeadBefore);
    assert.equal(headAfter, headBefore);
    assert.equal(statusBefore, '?? note.txt');
    assert.equal(statusAfter, '?? note.txt');
  } finally {
    if (previousLocalMainMode === undefined) {
      delete process.env.PLAN_LOCAL_MAIN_MODE;
    } else {
      process.env.PLAN_LOCAL_MAIN_MODE = previousLocalMainMode;
    }
  }
});
