/**
 * Unit tests for WorktreeManager.
 *
 * Tests worktree lifecycle: create, harvest, dedup, cleanup, prune.
 * Each test creates real git worktrees in a temporary repo.
 */

import { describe, test, expect, afterEach } from 'bun:test';
import { WorktreeManager } from '../lib/worktree';
import type { HarvestResult } from '../lib/worktree';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

/** Create a minimal git repo in a tmpdir for testing. */
function createTestRepo(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'worktree-test-'));
  spawnSync('git', ['init'], { cwd: dir, stdio: 'pipe' });
  spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir, stdio: 'pipe' });
  spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir, stdio: 'pipe' });

  // Create initial commit so HEAD exists
  fs.writeFileSync(path.join(dir, 'README.md'), '# Test repo\n');
  // Add .gitignore matching real repo (so copied build artifacts don't appear as changes)
  fs.writeFileSync(path.join(dir, '.gitignore'), '.agents/\nbrowse/dist/\n.gstack-worktrees/\n');
  // Create a .agents directory (simulating gitignored build artifacts)
  fs.mkdirSync(path.join(dir, '.agents', 'skills'), { recursive: true });
  fs.writeFileSync(path.join(dir, '.agents', 'skills', 'test-skill.md'), '# Test skill\n');
  // Create browse/dist (simulating build artifacts)
  fs.mkdirSync(path.join(dir, 'browse', 'dist'), { recursive: true });
  fs.writeFileSync(path.join(dir, 'browse', 'dist', 'browse'), '#!/bin/sh\necho browse\n');

  spawnSync('git', ['add', 'README.md', '.gitignore'], { cwd: dir, stdio: 'pipe' });
  spawnSync('git', ['commit', '-m', 'Initial commit'], { cwd: dir, stdio: 'pipe' });

  return dir;
}

/** Clean up a test repo. */
function cleanupRepo(dir: string): void {
  // Prune worktrees first to avoid git lock issues
  spawnSync('git', ['worktree', 'prune'], { cwd: dir, stdio: 'pipe' });
  fs.rmSync(dir, { recursive: true, force: true });
}

// Track repos to clean up
const repos: string[] = [];

// Dedup index path — clear before each test to avoid cross-run contamination
const DEDUP_PATH = path.join(os.homedir(), '.gstack-dev', 'harvests', 'dedup.json');

afterEach(() => {
  for (const repo of repos) {
    try { cleanupRepo(repo); } catch { /* best effort */ }
  }
  repos.length = 0;
  // Clear dedup index so tests are independent
  try { fs.unlinkSync(DEDUP_PATH); } catch { /* may not exist */ }
});

describe('WorktreeManager', () => {

  test('create() produces a valid worktree at the expected path', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-1');

    expect(fs.existsSync(worktreePath)).toBe(true);
    expect(fs.existsSync(path.join(worktreePath, 'README.md'))).toBe(true);
    expect(worktreePath).toContain('.gstack-worktrees');
    expect(worktreePath).toContain('test-1');

    mgr.cleanup('test-1');
  });

  test('create() worktree has .agents/skills/ (gitignored artifacts copied)', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-agents');

    expect(fs.existsSync(path.join(worktreePath, '.agents', 'skills', 'test-skill.md'))).toBe(true);
    expect(fs.existsSync(path.join(worktreePath, 'browse', 'dist', 'browse'))).toBe(true);

    mgr.cleanup('test-agents');
  });

  test('create() stores correct originalSha', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const expectedSha = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repo, stdio: 'pipe' })
      .stdout.toString().trim();

    mgr.create('test-sha');

    const info = mgr.getInfo('test-sha');
    expect(info).toBeDefined();
    expect(info!.originalSha).toBe(expectedSha);

    mgr.cleanup('test-sha');
  });

  test('harvest() captures modifications to tracked files', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-harvest-mod');

    // Modify a tracked file in the worktree
    fs.writeFileSync(path.join(worktreePath, 'README.md'), '# Modified!\n');

    const result = mgr.harvest('test-harvest-mod');

    expect(result).not.toBeNull();
    expect(result!.changedFiles).toContain('README.md');
    expect(result!.isDuplicate).toBe(false);
    expect(result!.patchPath).toBeTruthy();
    expect(fs.existsSync(result!.patchPath)).toBe(true);

    mgr.cleanup('test-harvest-mod');
  });

  test('harvest() captures new untracked files (git add -A path)', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-harvest-new');

    // Create a new file in the worktree
    fs.writeFileSync(path.join(worktreePath, 'new-file.txt'), 'Hello from agent\n');

    const result = mgr.harvest('test-harvest-new');

    expect(result).not.toBeNull();
    expect(result!.changedFiles).toContain('new-file.txt');

    mgr.cleanup('test-harvest-new');
  });

  test('harvest() captures committed changes (git diff originalSha)', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-harvest-commit');

    // Make a commit in the worktree (simulating agent running git commit)
    fs.writeFileSync(path.join(worktreePath, 'committed.txt'), 'Agent committed this\n');
    spawnSync('git', ['add', 'committed.txt'], { cwd: worktreePath, stdio: 'pipe' });
    spawnSync('git', ['commit', '-m', 'Agent commit'], { cwd: worktreePath, stdio: 'pipe' });

    const result = mgr.harvest('test-harvest-commit');

    expect(result).not.toBeNull();
    expect(result!.changedFiles).toContain('committed.txt');

    mgr.cleanup('test-harvest-commit');
  });

  test('harvest() returns null when worktree is clean', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    mgr.create('test-harvest-clean');

    // Don't modify anything
    const result = mgr.harvest('test-harvest-clean');

    expect(result).toBeNull();

    mgr.cleanup('test-harvest-clean');
  });

  test('harvest() dedup skips identical patches', () => {
    const repo = createTestRepo();
    repos.push(repo);

    // First run
    const mgr1 = new WorktreeManager(repo);
    const wt1 = mgr1.create('test-dedup-1');
    fs.writeFileSync(path.join(wt1, 'dedup-test.txt'), 'same content\n');
    const result1 = mgr1.harvest('test-dedup-1');
    mgr1.cleanup('test-dedup-1');

    expect(result1).not.toBeNull();
    expect(result1!.isDuplicate).toBe(false);

    // Second run with same change
    const mgr2 = new WorktreeManager(repo);
    const wt2 = mgr2.create('test-dedup-2');
    fs.writeFileSync(path.join(wt2, 'dedup-test.txt'), 'same content\n');
    const result2 = mgr2.harvest('test-dedup-2');
    mgr2.cleanup('test-dedup-2');

    expect(result2).not.toBeNull();
    expect(result2!.isDuplicate).toBe(true);
  });

  test('cleanup() removes worktree directory', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-cleanup');
    expect(fs.existsSync(worktreePath)).toBe(true);

    mgr.cleanup('test-cleanup');
    expect(fs.existsSync(worktreePath)).toBe(false);
  });

  test('pruneStale() removes orphaned worktrees from previous runs', () => {
    const repo = createTestRepo();
    repos.push(repo);

    // Create a worktree with a different manager (simulating a previous run)
    const oldMgr = new WorktreeManager(repo);
    const oldPath = oldMgr.create('stale-test');
    const oldRunDir = path.dirname(oldPath);
    expect(fs.existsSync(oldPath)).toBe(true);

    // Remove via git but leave directory (simulating a crash)
    spawnSync('git', ['worktree', 'remove', '--force', oldPath], { cwd: repo, stdio: 'pipe' });
    // Recreate the directory to simulate orphaned state
    fs.mkdirSync(oldPath, { recursive: true });
    // Backdate mtime to simulate a stale worktree (> 1 hour old)
    const staleTime = new Date(Date.now() - 7200_000);
    fs.utimesSync(oldRunDir, staleTime, staleTime);

    // New manager should prune the old run's directory
    const newMgr = new WorktreeManager(repo);
    newMgr.pruneStale();

    expect(fs.existsSync(oldRunDir)).toBe(false);
  });

  test('create() throws on failure (no silent fallback to ROOT)', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    // Create the same worktree twice — second should fail because path exists
    mgr.create('test-fail');
    expect(() => mgr.create('test-fail')).toThrow();

    mgr.cleanup('test-fail');
  });

  test('harvest() returns null gracefully when worktree dir was deleted by agent', () => {
    const repo = createTestRepo();
    repos.push(repo);
    const mgr = new WorktreeManager(repo);

    const worktreePath = mgr.create('test-deleted');

    // Simulate agent deleting its own worktree directory
    fs.rmSync(worktreePath, { recursive: true, force: true });

    // harvest should return null gracefully, not throw
    const result = mgr.harvest('test-deleted');
    expect(result).toBeNull();

    // cleanup should also be non-fatal
    mgr.cleanup('test-deleted');
  });
});
