import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';

import { getProjectVersion, removeVersion } from '../versions.js';
import { getVersionsDir, getTrashVersionsDir } from '../state.js';
import { getDB, updateSessionFilePaths } from '../session/db.js';
import type { AgentId } from '../types.js';

let tmpDir: string;

beforeEach(() => {
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'versions-test-'));
});

afterEach(() => {
  fs.rmSync(tmpDir, { recursive: true, force: true });
});

describe('getProjectVersion', () => {
  it('resolves version from agents.yaml in startPath', () => {
    fs.writeFileSync(
      path.join(tmpDir, 'agents.yaml'),
      'agents:\n  claude: "1.2.3"\n',
    );
    expect(getProjectVersion('claude', tmpDir)).toBe('1.2.3');
  });

  it('walks up to find agents.yaml in a parent directory', () => {
    const nested = path.join(tmpDir, 'a', 'b', 'c');
    fs.mkdirSync(nested, { recursive: true });
    fs.writeFileSync(
      path.join(tmpDir, 'agents.yaml'),
      'agents:\n  claude: "2.0.0"\n',
    );
    expect(getProjectVersion('claude', nested)).toBe('2.0.0');
  });

  it('returns null when no agents.yaml exists', () => {
    const nested = path.join(tmpDir, 'empty');
    fs.mkdirSync(nested, { recursive: true });
    expect(getProjectVersion('claude', nested)).toBeNull();
  });

  it('returns null when agents.yaml exists but agent key is absent', () => {
    fs.writeFileSync(
      path.join(tmpDir, 'agents.yaml'),
      'agents:\n  codex: "0.9.0"\n',
    );
    expect(getProjectVersion('claude', tmpDir)).toBeNull();
  });

  it('ignores ~/.agents-system/ root when walking up', () => {
    // getProjectAgentsDir must skip both the system root (~/.agents-system/) and the
    // user root (~/.agents/) so they are never returned as project-scoped dirs.
    // We cannot mutate os.homedir(), but we CAN verify that starting the search
    // from inside ~/.agents-system/ returns null (no project version).
    const systemAgentsYaml = path.join(os.homedir(), '.agents-system', 'agents.yaml');
    if (fs.existsSync(systemAgentsYaml)) {
      const result = getProjectVersion('claude', path.dirname(systemAgentsYaml));
      expect(typeof result === 'string' || result === null).toBe(true);
    } else {
      expect(true).toBe(true);
    }
  });

  it('parses agents.yaml with quoted version string', () => {
    fs.writeFileSync(
      path.join(tmpDir, 'agents.yaml'),
      'agents:\n  claude: \'1.5.0\'\n',
    );
    expect(getProjectVersion('claude', tmpDir)).toBe('1.5.0');
  });

  it('parses agents.yaml with unquoted version string', () => {
    fs.writeFileSync(
      path.join(tmpDir, 'agents.yaml'),
      'agents:\n  claude: 1.7.2\n',
    );
    expect(getProjectVersion('claude', tmpDir)).toBe('1.7.2');
  });
});

// Scenario: the user uninstalls an agent version. Their conversation history,
// which lives at .../versions/<agent>/<version>/home/, must survive — now
// inside the trash directory because removeVersion is a soft-delete: the
// entire version dir (including home/) is renamed to
// ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/.
// Verified for every AgentId since removeVersion is parametrised on agent and
// the layout is shared across all agents.
describe('removeVersion soft-deletes the entire version dir to trash', () => {
  const cases: { agent: AgentId; historyDir: string; binaryName: string }[] = [
    { agent: 'claude',   historyDir: path.join('home', '.claude',   'projects'), binaryName: 'claude' },
    { agent: 'codex',    historyDir: path.join('home', '.codex',    'sessions'), binaryName: 'codex' },
    { agent: 'gemini',   historyDir: path.join('home', '.gemini',   'sessions'), binaryName: 'gemini' },
    { agent: 'cursor',   historyDir: path.join('home', '.cursor',   'sessions'), binaryName: 'cursor-agent' },
    { agent: 'opencode', historyDir: path.join('home', '.opencode', 'sessions'), binaryName: 'opencode' },
    { agent: 'openclaw', historyDir: path.join('home', '.openclaw', 'sessions'), binaryName: 'openclaw' },
  ];

  for (const { agent, historyDir, binaryName } of cases) {
    it(`[${agent}] moves whole versionDir to trash; binaries AND home/ survive there`, () => {
      const testVersion = `0.0.0-test-${crypto.randomBytes(4).toString('hex')}`;
      const versionDir = path.join(getVersionsDir(), agent, testVersion);
      const trashAgentDir = path.join(getTrashVersionsDir(), agent, testVersion);

      try {
        fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
        fs.writeFileSync(path.join(versionDir, 'node_modules', '.bin', binaryName), '#!/bin/sh\n');
        fs.writeFileSync(path.join(versionDir, 'package.json'), '{}');
        fs.writeFileSync(path.join(versionDir, 'package-lock.json'), '{}');

        const sessionDir = path.join(versionDir, historyDir);
        fs.mkdirSync(sessionDir, { recursive: true });
        const sessionRel = path.relative(versionDir, path.join(sessionDir, 'session.jsonl'));
        fs.writeFileSync(path.join(versionDir, sessionRel), '{"type":"user"}\n');

        expect(removeVersion(agent, testVersion)).toBe(true);

        // Original location is gone — soft-delete renamed it.
        expect(fs.existsSync(versionDir)).toBe(false);

        // Trash now contains exactly one timestamped copy with everything.
        const stamps = fs.readdirSync(trashAgentDir);
        expect(stamps.length).toBe(1);
        const trashed = path.join(trashAgentDir, stamps[0]);

        expect(fs.existsSync(path.join(trashed, 'node_modules', '.bin', binaryName))).toBe(true);
        expect(fs.existsSync(path.join(trashed, 'package.json'))).toBe(true);
        expect(fs.existsSync(path.join(trashed, sessionRel))).toBe(true);
      } finally {
        if (fs.existsSync(versionDir)) {
          fs.rmSync(versionDir, { recursive: true, force: true });
        }
        if (fs.existsSync(trashAgentDir)) {
          fs.rmSync(trashAgentDir, { recursive: true, force: true });
        }
      }
    });
  }
});

// When a version is removed, the command handler calls removeVersion then
// updateSessionFilePaths so session reads still work from the new trash location.
// This test verifies both pieces independently and together.
describe('updateSessionFilePaths rewrites file_path after soft-delete to trash', () => {
  const testVersion = `0.0.0-dbtest-${crypto.randomBytes(4).toString('hex')}`;
  const testSessionId = `test-session-${crypto.randomBytes(8).toString('hex')}`;
  const agent: AgentId = 'claude';
  const binaryName = 'claude';

  afterEach(() => {
    // Clean up the test session row from the real DB
    try {
      const db = getDB();
      db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(testSessionId);
      db.prepare(`DELETE FROM sessions WHERE id = ?`).run(testSessionId);
    } catch { /* ignore */ }

    // Clean up any leftover version or trash dirs
    const versionDir = path.join(getVersionsDir(), agent, testVersion);
    const trashAgentDir = path.join(getTrashVersionsDir(), agent, testVersion);
    if (fs.existsSync(versionDir)) fs.rmSync(versionDir, { recursive: true, force: true });
    if (fs.existsSync(trashAgentDir)) fs.rmSync(trashAgentDir, { recursive: true, force: true });
  });

  it('rewrites file_path to trash location after removeVersion + updateSessionFilePaths', () => {
    const versionDir = path.join(getVersionsDir(), agent, testVersion);
    const sessionJsonl = path.join(versionDir, 'home', '.claude', 'projects', 'test', `${testSessionId}.jsonl`);

    // Set up a minimal version dir so removeVersion has something to soft-delete
    fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
    fs.writeFileSync(path.join(versionDir, 'node_modules', '.bin', binaryName), '#!/bin/sh\n');
    fs.writeFileSync(path.join(versionDir, 'package.json'), '{}');
    fs.writeFileSync(path.join(versionDir, 'package-lock.json'), '{}');
    fs.mkdirSync(path.dirname(sessionJsonl), { recursive: true });
    fs.writeFileSync(sessionJsonl, '{"type":"user"}\n');

    // Insert a session row pointing at the version-dir path
    const db = getDB();
    db.prepare(`
      INSERT OR REPLACE INTO sessions
        (id, short_id, agent, timestamp, file_path, is_team_origin)
      VALUES (?, ?, ?, ?, ?, 0)
    `).run(testSessionId, testSessionId.slice(0, 8), 'claude', new Date().toISOString(), sessionJsonl);

    // Soft-delete the version (mirrors what the command handler does)
    expect(removeVersion(agent, testVersion)).toBe(true);

    // The trash dir now holds a timestamped copy
    const trashAgentDir = path.join(getTrashVersionsDir(), agent, testVersion);
    const stamps = fs.readdirSync(trashAgentDir).sort().reverse();
    expect(stamps.length).toBeGreaterThan(0);
    const trashVersionDir = path.join(trashAgentDir, stamps[0]);

    // Simulate what the command handler does: rewrite file_paths to trash location
    const updated = updateSessionFilePaths(versionDir, trashVersionDir);
    expect(updated).toBe(1);

    // DB file_path must now point into the trash location
    const row = db.prepare(`SELECT file_path FROM sessions WHERE id = ?`).get(testSessionId) as { file_path: string } | undefined;
    expect(row).toBeDefined();
    expect(row!.file_path.startsWith(trashVersionDir)).toBe(true);

    // The file must actually exist at the new path
    expect(fs.existsSync(row!.file_path)).toBe(true);
  });
});
