// src/engine/reflection/silent-fork.test.ts

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import matter from 'gray-matter';
// This mock is intentionally file-wide. vi.mock is hoisted, so keeping it at
// top level makes the actual test behavior explicit and Vitest 4 compatible.
vi.mock('child_process', () => ({
  execSync: () => {
    throw new Error('git: command not found');
  },
}));


// ---- helpers ----------------------------------------------------------------

function makeTempDir(): string {
  return mkdtempSync(join(tmpdir(), 'silent-fork-test-'));
}

function writeBuiltin(builtinDir: string, name: string, content: string): void {
  mkdirSync(builtinDir, { recursive: true });
  writeFileSync(join(builtinDir, `${name}.yaml`), content);
}

// ---- tests ------------------------------------------------------------------

describe('silentFork', () => {
  let dir: string;
  let builtinDir: string;
  let dataDir: string;

  beforeEach(() => {
    dir = makeTempDir();
    builtinDir = join(dir, 'pieces');
    dataDir = join(dir, 'data', 'users');
    mkdirSync(builtinDir, { recursive: true });
    mkdirSync(dataDir, { recursive: true });
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
    vi.restoreAllMocks();
  });

  it('copies the built-in piece into the user dir', async () => {
    const { silentFork } = await import('./silent-fork.js');
    writeBuiltin(builtinDir, 'chat', 'movements:\n  - name: execute\n');

    const result = silentFork(builtinDir, dataDir, 'user1', 'chat');

    expect(result.forked).toBe(true);
    const dstPath = join(dataDir, 'user1', 'pieces', 'chat.yaml');
    expect(existsSync(dstPath)).toBe(true);
  });

  it('records forked_from_commit in frontmatter (round-trip via gray-matter)', async () => {
    const { silentFork } = await import('./silent-fork.js');
    writeBuiltin(builtinDir, 'research', 'movements:\n  - name: execute\n');

    const result = silentFork(builtinDir, dataDir, 'user1', 'research');

    expect(result.forked).toBe(true);
    const dstPath = join(dataDir, 'user1', 'pieces', 'research.yaml');
    const written = readFileSync(dstPath, 'utf-8');
    const parsed = matter(written);

    // forked_from_commit must be a non-empty string (either a SHA or 'unknown').
    expect(typeof parsed.data.forked_from_commit).toBe('string');
    expect(parsed.data.forked_from_commit.length).toBeGreaterThan(0);

    // The return value's commit must be consistent with the stored value.
    if (result.commit !== null) {
      expect(parsed.data.forked_from_commit).toBe(result.commit);
    } else {
      expect(parsed.data.forked_from_commit).toBe('unknown');
    }
  });

  it('records forked_at as an ISO-8601 timestamp', async () => {
    const { silentFork } = await import('./silent-fork.js');
    const before = new Date();
    writeBuiltin(builtinDir, 'general', 'movements:\n  - name: execute\n');

    silentFork(builtinDir, dataDir, 'user1', 'general');

    const after = new Date();
    const dstPath = join(dataDir, 'user1', 'pieces', 'general.yaml');
    const written = readFileSync(dstPath, 'utf-8');
    const parsed = matter(written);

    expect(typeof parsed.data.forked_at).toBe('string');
    const ts = new Date(parsed.data.forked_at as string);
    expect(ts.getTime()).toBeGreaterThanOrEqual(before.getTime() - 1000);
    expect(ts.getTime()).toBeLessThanOrEqual(after.getTime() + 1000);
  });

  it('is a no-op when the custom version already exists', async () => {
    const { silentFork } = await import('./silent-fork.js');
    writeBuiltin(builtinDir, 'slide', 'movements:\n  - name: execute\n');

    // First call creates the fork.
    const first = silentFork(builtinDir, dataDir, 'user1', 'slide');
    expect(first.forked).toBe(true);

    // Record the content after the first fork.
    const dstPath = join(dataDir, 'user1', 'pieces', 'slide.yaml');
    const originalContent = readFileSync(dstPath, 'utf-8');

    // Second call must be a no-op.
    const second = silentFork(builtinDir, dataDir, 'user1', 'slide');
    expect(second.forked).toBe(false);
    expect(second.commit).toBeNull();

    // File content must be unchanged.
    expect(readFileSync(dstPath, 'utf-8')).toBe(originalContent);
  });

  it('throws when the built-in piece is not found', async () => {
    const { silentFork } = await import('./silent-fork.js');

    expect(() => silentFork(builtinDir, dataDir, 'user1', 'nonexistent')).toThrow(
      /built-in piece not found: nonexistent/
    );
  });

  it('preserves existing body content after stamping frontmatter', async () => {
    const { silentFork } = await import('./silent-fork.js');
    const body = 'movements:\n  - name: execute\n    rules:\n      - next: done\n';
    writeBuiltin(builtinDir, 'office-process', body);

    silentFork(builtinDir, dataDir, 'user1', 'office-process');

    const dstPath = join(dataDir, 'user1', 'pieces', 'office-process.yaml');
    const written = readFileSync(dstPath, 'utf-8');
    const parsed = matter(written);

    // The YAML body (non-frontmatter content) must be preserved.
    expect(parsed.content.trim()).toContain('movements');
  });
});

// ---- git-unavailable suite --------------------------------------------------
describe('silentFork — git unavailable', () => {
  let dir: string;
  let builtinDir: string;
  let dataDir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'silent-fork-nogit-'));
    builtinDir = join(dir, 'pieces');
    dataDir = join(dir, 'data', 'users');
    mkdirSync(builtinDir, { recursive: true });
    mkdirSync(dataDir, { recursive: true });
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  it('still copies the file when git is not available (commit is null)', async () => {
    writeFileSync(join(builtinDir, 'data-process.yaml'), 'movements:\n  - name: execute\n');

    // Re-import to pick up the mocked child_process.
    const mod = await import('./silent-fork.js');
    const result = mod.silentFork(builtinDir, dataDir, 'user1', 'data-process');

    expect(result.forked).toBe(true);
    expect(result.commit).toBeNull();

    const dstPath = join(dataDir, 'user1', 'pieces', 'data-process.yaml');
    expect(existsSync(dstPath)).toBe(true);

    // forked_from_commit must be 'unknown' when git is unavailable.
    const written = readFileSync(dstPath, 'utf-8');
    const parsed = matter(written);
    expect(parsed.data.forked_from_commit).toBe('unknown');
  });
});
