import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { rm, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import postgres from 'postgres';
import {
  ensureSessionWorktree,
  closeChatBackendState,
  rebaselineWorktreeAfterApply,
} from '../worktrees.js';
import { reapOrphanWorktrees } from '../orphan-worktree-reaper.js';
import { hostExec } from '../host-exec.js';

/**
 * v2.6 Phase 3 (3.6) — reconnect-after-restart integration test.
 *
 * Proves the DB-truth side of crash/restart recovery: a BooCoder restart wipes the
 * in-memory pool, but the persistent `worktrees` + `agent_sessions` rows survive,
 * so the "next turn" re-resolves the SAME worktree (reattach, no new dir) and the
 * agent-session row is still there to resume from. Also exercises the chat-close
 * hook (3.3), the apply re-baseline (3.5), and the orphan reaper (3.4) end-to-end
 * against a real git repo + postgres.
 *
 * Requires DATABASE_URL (DB-opt-in; skips cleanly otherwise) AND git on PATH. Runs:
 *   DATABASE_URL='postgres://boocode:devpass@localhost:5500/boochat' pnpm -C apps/coder test
 */
describe.runIf(!!process.env.DATABASE_URL)('reconnect after restart (Phase 3)', () => {
  let sql: ReturnType<typeof postgres>;
  const stamp = Date.now();
  const projectDir = `/tmp/boocode-reconnect-proj-${stamp}`;
  let projectId: string;
  let sessionId: string;
  let chatId: string;

  beforeAll(async () => {
    sql = postgres(process.env.DATABASE_URL!, { max: 3 });

    // Both schemas land in the one boochat DB: server owns sessions/chats/projects,
    // coder owns worktrees/agent_sessions (FK targets must pre-exist → server first).
    const serverSchema = resolve(__dirname, '../../../../server/src/schema.sql');
    const coderSchema = resolve(__dirname, '../../schema.sql');
    await sql.unsafe(readFileSync(serverSchema, 'utf8'));
    await sql.unsafe(readFileSync(coderSchema, 'utf8'));

    // A real git repo with one commit so worktree add / diff / rev-parse work.
    await mkdir(projectDir, { recursive: true });
    await hostExec(
      `cd ${projectDir} && git init -q && git config user.email t@t && git config user.name t ` +
        `&& echo hello > README.md && git add -A && git commit -qm init`,
      { timeoutMs: 20_000 },
    );

    const [project] = await sql<{ id: string }[]>`
      INSERT INTO projects (name, path, status) VALUES ('reconnect-test', ${projectDir}, 'open') RETURNING id
    `;
    projectId = project!.id;
    const [session] = await sql<{ id: string }[]>`
      INSERT INTO sessions (project_id, name, model, status)
      VALUES (${projectId}, 'recon', 'm', 'open') RETURNING id
    `;
    sessionId = session!.id;
    const [chat] = await sql<{ id: string }[]>`
      INSERT INTO chats (session_id, name, status) VALUES (${sessionId}, 'tab', 'open') RETURNING id
    `;
    chatId = chat!.id;
  });

  afterAll(async () => {
    if (sql) {
      // Best-effort worktree cleanup before dropping rows.
      const rows = await sql<{ path: string }[]>`SELECT path FROM worktrees WHERE session_id = ${sessionId}`.catch(() => []);
      for (const r of rows) {
        await hostExec(`git -C ${projectDir} worktree remove ${r.path} --force`, { timeoutMs: 10_000 }).catch(() => {});
      }
      await sql`DELETE FROM agent_sessions WHERE chat_id = ${chatId}`.catch(() => {});
      await sql`DELETE FROM worktrees WHERE session_id = ${sessionId}`.catch(() => {});
      await sql`DELETE FROM chats WHERE id = ${chatId}`.catch(() => {});
      await sql`DELETE FROM sessions WHERE id = ${sessionId}`.catch(() => {});
      await sql`DELETE FROM projects WHERE id = ${projectId}`.catch(() => {});
      await sql.end({ timeout: 5 });
    }
    await rm(projectDir, { recursive: true, force: true });
  });

  it('reattaches the SAME worktree across a simulated restart (no new dir)', async () => {
    // "Turn 1" — first ensureSessionWorktree creates the worktree + row.
    const first = await ensureSessionWorktree(sql, projectDir, sessionId);
    expect(existsSync(first.worktreePath)).toBe(true);
    expect(first.baseCommit).toBeTruthy();

    // Simulate an agent_sessions row written by turn 1 (opencode).
    await sql`
      INSERT INTO agent_sessions (chat_id, session_id, worktree_id, agent, backend, agent_session_id, status, last_active_at)
      VALUES (${chatId}, ${sessionId}, ${first.worktreeId}, 'opencode', 'opencode_server', 'oc-sess-1', 'active', clock_timestamp())
      ON CONFLICT (chat_id, agent) DO NOTHING
    `;

    // "Restart" = brand-new resolution with NO in-memory state. ensureSessionWorktree
    // must return the EXISTING row (same id + path), proving reattach not re-create.
    const second = await ensureSessionWorktree(sql, projectDir, sessionId);
    expect(second.worktreeId).toBe(first.worktreeId);
    expect(second.worktreePath).toBe(first.worktreePath);
    expect(second.baseCommit).toBe(first.baseCommit);

    // The agent_sessions row survived the "restart" with its resume handle intact.
    const [row] = await sql<{ agent_session_id: string; status: string }[]>`
      SELECT agent_session_id, status FROM agent_sessions WHERE chat_id = ${chatId} AND agent = 'opencode'
    `;
    expect(row!.agent_session_id).toBe('oc-sess-1');
  });

  it('re-baselines the worktree diff after apply (3.5)', async () => {
    const wt = await ensureSessionWorktree(sql, projectDir, sessionId);
    const baseBefore = wt.baseCommit;
    // Make a change in the worktree (as an external agent would).
    await hostExec(`cd ${wt.worktreePath} && echo change >> README.md`, { timeoutMs: 10_000 });

    const r = await rebaselineWorktreeAfterApply(sql, sessionId);
    expect(r.rebaselined).toBe(true);
    expect(r.newBaseCommit).toBeTruthy();
    expect(r.newBaseCommit).not.toBe(baseBefore);

    const [row] = await sql<{ base_commit: string }[]>`
      SELECT base_commit FROM worktrees WHERE session_id = ${sessionId} AND status = 'active'
    `;
    expect(row!.base_commit).toBe(r.newBaseCommit);

    // Idempotent: a second re-baseline with no new edits is a no-op.
    const r2 = await rebaselineWorktreeAfterApply(sql, sessionId);
    expect(r2.rebaselined).toBe(false);
  });

  it('chat-close hook closes agent rows + removes the worktree on the last chat (3.3)', async () => {
    // Sanity: an active worktree + agent row exist from the prior tests.
    const beforeWt = await sql<{ id: string }[]>`SELECT id FROM worktrees WHERE session_id = ${sessionId} AND status = 'active'`;
    expect(beforeWt.length).toBe(1);

    const result = await closeChatBackendState(sql, chatId);
    expect(result.agentRowsClosed).toBeGreaterThanOrEqual(1);
    // chatId is the session's only chat → worktree removed (it was clean after the
    // re-baseline commit), not at-risk.
    expect(result.worktreeAtRisk).toBe(false);
    expect(result.worktreeRemoved).toBe(true);

    const [agentRow] = await sql<{ status: string }[]>`
      SELECT status FROM agent_sessions WHERE chat_id = ${chatId} AND agent = 'opencode'
    `;
    expect(agentRow!.status).toBe('closed');

    const activeWt = await sql<{ id: string }[]>`SELECT id FROM worktrees WHERE session_id = ${sessionId} AND status = 'active'`;
    expect(activeWt.length).toBe(0); // archived, no longer active
  });

  it('orphan reaper leaves a live worktree alone and reaps a row-less dir (3.4)', async () => {
    // Recreate a live worktree for this session (the close test archived the old one).
    const live = await ensureSessionWorktree(sql, projectDir, sessionId);
    expect(existsSync(live.worktreePath)).toBe(true);

    // A live worktree (active row) with grace 0 must NOT be reaped.
    const r1 = await reapOrphanWorktrees(sql, console as never, 0, Date.now());
    expect(r1.reaped).not.toContain(live.worktreePath);

    // Now archive its row (simulating a leaked dir) and reap again — it becomes an
    // orphan and is reclaimed (it's clean → not at-risk).
    await sql`UPDATE worktrees SET status = 'archived' WHERE id = ${live.worktreeId}`;
    const r2 = await reapOrphanWorktrees(sql, console as never, 0, Date.now());
    expect(r2.reaped).toContain(live.worktreePath);
    expect(existsSync(live.worktreePath)).toBe(false);
  });
});
