import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, basename, dirname, resolve } from 'node:path';
import type { RepoSummary, WorktreeSummary } from '@arboretum/shared';
import { WorktreeManager } from '../src/core/worktree-manager.js';
import { PtyManager } from '../src/core/pty-manager.js';
import { DiscoveryService } from '../src/core/discovery-service.js';
import { openDb, type Db } from '../src/db/index.js';

// node-pty inerte (la corrélation de session n'a besoin que d'un pid et d'un cwd).
vi.mock('@homebridge/node-pty-prebuilt-multiarch', () => {
  let pid = 50_000;
  class FakePty {
    pid = pid++;
    write = vi.fn();
    resize = vi.fn();
    pause = vi.fn();
    resume = vi.fn();
    kill = vi.fn();
    onData(): { dispose: () => void } {
      return { dispose: () => {} };
    }
    onExit(): { dispose: () => void } {
      return { dispose: () => {} };
    }
  }
  return { default: { spawn: (): FakePty => new FakePty() } };
});

const dirs: string[] = [];
afterEach(() => {
  for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
});

function gitInit(dir: string): void {
  const run = (...args: string[]): void => void execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
  run('init', '-b', 'main');
  run('config', 'user.email', 'test@arboretum.dev');
  run('config', 'user.name', 'Test');
  writeFileSync(join(dir, 'README.md'), '# test\n');
  run('add', '-A');
  run('commit', '-m', 'init');
}

function makeTmpRepo(): string {
  const dir = mkdtempSync(join(tmpdir(), 'arb-wtm-'));
  dirs.push(dir);
  gitInit(dir);
  return dir;
}

/** Crée un vrai repo git à un chemin donné (sous une racine de scan contrôlée). */
function makeRepoAt(path: string): void {
  mkdirSync(path, { recursive: true });
  gitInit(path);
}

describe('WorktreeManager', () => {
  let db: Db;
  let pty: PtyManager;
  let discovery: DiscoveryService;
  let wt: WorktreeManager;
  let claudeHome: string;

  beforeEach(() => {
    db = openDb(':memory:');
    claudeHome = mkdtempSync(join(tmpdir(), 'arb-ch-'));
    dirs.push(claudeHome);
    pty = new PtyManager(db, join(claudeHome, 'sessions'));
    discovery = new DiscoveryService({ ptyManager: pty, projectsDir: join(claudeHome, 'projects'), sessionsDir: join(claudeHome, 'sessions') });
    wt = new WorktreeManager(db, pty, discovery);
  });

  it('addRepo : repo valide enregistré ; non-repo rejeté (400)', async () => {
    const repo = makeTmpRepo();
    const summary = await wt.addRepo({ path: repo });
    expect(summary).toMatchObject({ path: repo, label: basename(repo), valid: true });
    expect((await wt.listRepos())).toHaveLength(1);

    const notRepo = mkdtempSync(join(tmpdir(), 'arb-nogit-'));
    dirs.push(notRepo);
    await expect(wt.addRepo({ path: notRepo })).rejects.toMatchObject({ statusCode: 400 });
    // doublon
    await expect(wt.addRepo({ path: repo })).rejects.toMatchObject({ statusCode: 409 });
  });

  it('createWorktree : worktree + hook exécuté + event worktree_update', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({
      path: repo,
      postCreateHooks: [{ id: 'h1', label: 'touch', run: 'touch hook-ran.txt', enabled: true }],
    });
    const events: Array<{ repoId: string; worktree: WorktreeSummary }> = [];
    wt.on('worktree_update', (e) => events.push(e));

    const wtPath = join(dirname(repo), `${basename(repo)}-wt-feat`);
    dirs.push(wtPath);
    const out = await wt.createWorktree(r.id, { branch: 'feat', newBranch: true, runHooks: true });

    expect(resolve(out.worktree.path)).toBe(resolve(wtPath));
    expect(out.worktree.branch).toBe('feat');
    expect(out.hookResults).toHaveLength(1);
    expect(out.hookResults[0]).toMatchObject({ exitCode: 0 });
    expect(existsSync(join(wtPath, 'hook-ran.txt'))).toBe(true);
    expect(events.some((e) => resolve(e.worktree.path) === resolve(wtPath))).toBe(true);

    const list = await wt.listRepoWorktrees(r.id, true);
    expect(list.some((w) => resolve(w.path) === resolve(wtPath))).toBe(true);
  });

  it('createWorktree : branche invalide → 400', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    await expect(wt.createWorktree(r.id, { branch: '../evil', newBranch: true })).rejects.toMatchObject({ statusCode: 400 });
  });

  it('deleteWorktree : main refusé (400), dirty refusé (409) puis force OK', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    await expect(wt.deleteWorktree(r.id, repo, false)).rejects.toMatchObject({ statusCode: 400, code: 'IS_MAIN_WORKTREE' });

    const wtPath = join(dirname(repo), `${basename(repo)}-wt-x`);
    dirs.push(wtPath);
    await wt.createWorktree(r.id, { branch: 'x', newBranch: true, runHooks: false });
    writeFileSync(join(wtPath, 'scratch.txt'), 'wip\n'); // worktree sale

    await expect(wt.deleteWorktree(r.id, wtPath, false)).rejects.toMatchObject({ statusCode: 409, code: 'WORKTREE_DIRTY' });
    await wt.deleteWorktree(r.id, wtPath, true);
    expect((await wt.listRepoWorktrees(r.id, true)).some((w) => resolve(w.path) === resolve(wtPath))).toBe(false);
  });

  it('corrélation : une session dont le cwd = worktree apparaît dans worktree.sessions', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    const wtPath = join(dirname(repo), `${basename(repo)}-wt-sess`);
    dirs.push(wtPath);
    await wt.createWorktree(r.id, { branch: 'sess', newBranch: true, runHooks: false });

    pty.spawn({ cwd: wtPath, command: 'bash' });
    const list = await wt.listRepoWorktrees(r.id, true);
    const target = list.find((w) => resolve(w.path) === resolve(wtPath));
    expect(target?.sessions).toHaveLength(1);
    expect(target?.sessions[0]).toMatchObject({ source: 'managed', live: true });
  });

  it('deleteWorktree : session live dans le worktree → 409 sans force', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    const wtPath = join(dirname(repo), `${basename(repo)}-wt-busy`);
    dirs.push(wtPath);
    await wt.createWorktree(r.id, { branch: 'busy', newBranch: true, runHooks: false });
    pty.spawn({ cwd: wtPath, command: 'bash' });
    await expect(wt.deleteWorktree(r.id, wtPath, false)).rejects.toMatchObject({ statusCode: 409, code: 'SESSION_LIVE_IN_WORKTREE' });
  });

  it('addRepo renvoie hidden:false ; updateRepo({hidden}) bascule et émet repo_update', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    expect(r.hidden).toBe(false);
    const updates: RepoSummary[] = [];
    wt.on('repo_update', (s) => updates.push(s));
    const u = await wt.updateRepo(r.id, { hidden: true });
    expect(u.hidden).toBe(true);
    expect(updates.some((s) => s.id === r.id && s.hidden)).toBe(true);
  });

  it('listAllWorktrees exclut les repos masqués', async () => {
    const repo = makeTmpRepo();
    const r = await wt.addRepo({ path: repo });
    expect((await wt.listAllWorktrees()).length).toBeGreaterThan(0); // main worktree présent
    await wt.updateRepo(r.id, { hidden: true });
    expect(await wt.listAllWorktrees()).toHaveLength(0);
  });

  it('discoverRepos : auto-ajoute les nouveaux, idempotent, masqué non ressuscité, supprimé re-découvrable', async () => {
    const root = mkdtempSync(join(tmpdir(), 'arb-scan-'));
    dirs.push(root);
    makeRepoAt(join(root, 'a'));
    makeRepoAt(join(root, 'b'));

    const updates: RepoSummary[] = [];
    wt.on('repo_update', (s) => updates.push(s));

    const res = await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(res.added).toBe(2);
    expect(res.scanned).toBe(2);
    expect(await wt.listRepos()).toHaveLength(2);
    expect(updates).toHaveLength(2); // un repo_update par nouveau

    // re-scan : idempotent (aucun ajout, aucune émission)
    updates.length = 0;
    const res2 = await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(res2.added).toBe(0);
    expect(updates).toHaveLength(0);
    expect(await wt.listRepos()).toHaveLength(2);

    // masquer 'a' puis re-scan : reste masqué, jamais ré-ajouté (invariant central)
    const repoA = (await wt.listRepos()).find((r) => resolve(r.path) === resolve(join(root, 'a')));
    await wt.updateRepo(repoA!.id, { hidden: true });
    updates.length = 0;
    const res3 = await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(res3.added).toBe(0);
    expect((await wt.listRepos()).find((r) => r.id === repoA!.id)?.hidden).toBe(true);

    // supprimer 'b' puis re-scan : re-découvert (volontaire)
    const repoB = (await wt.listRepos()).find((r) => resolve(r.path) === resolve(join(root, 'b')));
    wt.removeRepo(repoB!.id);
    expect(await wt.listRepos()).toHaveLength(1);
    const res4 = await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(res4.added).toBe(1);
    expect(await wt.listRepos()).toHaveLength(2);
  });

  it('discoverRepos : repo disparu du disque conservé en DB (valid=false), non re-trouvé', async () => {
    const root = mkdtempSync(join(tmpdir(), 'arb-scan-'));
    dirs.push(root);
    makeRepoAt(join(root, 'gone'));
    await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(await wt.listRepos()).toHaveLength(1);

    rmSync(join(root, 'gone'), { recursive: true, force: true }); // disparaît du disque
    const res = await wt.discoverRepos({ roots: [root], maxDepth: 2 });
    expect(res.added).toBe(0); // plus trouvé par le scan
    const repos = await wt.listRepos();
    expect(repos).toHaveLength(1); // mais la ligne est conservée (pas de suppression auto)
    expect(repos[0].valid).toBe(false);
    // robustesse : un repo dont le chemin a disparu ne fait pas planter /worktrees (git échoue → [])
    await expect(wt.listAllWorktrees()).resolves.toEqual([]);
  });
});
