import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, cpSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { executeTool } from './pieces.js';
import type { ToolContext } from './core.js';

/**
 * The pieces tool resolves BUILTIN_PIECES_DIR from process.cwd() at module
 * load time. Vitest workers don't support process.chdir, so we run the
 * suite against the project's actual `pieces/` directory and use a known
 * real built-in name (`chat`) for the "refuse to overwrite built-in" path.
 * CreatePiece writes go to a fresh customPiecesDir each test.
 */

const VALID_NEW_PIECE_YAML = `name: test-fresh-piece
description: example
max_movements: 10
initial_movement: do
movements:
  - name: do
    edit: false
    persona: tester
    instruction: do
    allowed_tools: []
    rules:
      - condition: done
        next: do
`;

let customDir: string;

function ctx(): ToolContext {
  return {
    workspacePath: '/tmp/dummy',
    editAllowed: false,
    customPiecesDir: customDir,
  };
}

beforeEach(() => {
  customDir = mkdtempSync(join(tmpdir(), 'pieces-custom-'));
});

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

describe('CreatePiece', () => {
  it('rejects YAML missing max_movements (regression for "Exceeded max movements (undefined)")', async () => {
    const yaml = `name: test-no-mm
description: missing the cap
initial_movement: do
movements:
  - name: do
    edit: false
    persona: t
    instruction: t
    allowed_tools: []
    rules:
      - condition: done
        next: do
`;
    const result = await executeTool('CreatePiece', { yaml_content: yaml }, ctx());
    expect(result?.isError).toBe(true);
    expect(result?.output).toMatch(/max_movements is required/);
    expect(existsSync(join(customDir, 'test-no-mm.yaml'))).toBe(false);
  });

  it('rejects max_movements: 0', async () => {
    const yaml = VALID_NEW_PIECE_YAML
      .replace('max_movements: 10', 'max_movements: 0')
      .replace('test-fresh-piece', 'test-zero-mm');
    const result = await executeTool('CreatePiece', { yaml_content: yaml }, ctx());
    expect(result?.isError).toBe(true);
    expect(result?.output).toMatch(/max_movements/);
  });

  it('accepts a valid piece and writes to customPiecesDir', async () => {
    const result = await executeTool(
      'CreatePiece',
      { yaml_content: VALID_NEW_PIECE_YAML },
      ctx(),
    );
    expect(result?.isError).toBe(false);
    expect(existsSync(join(customDir, 'test-fresh-piece.yaml'))).toBe(true);
  });
});

describe('customPiecesDir as array — P1-a regression', () => {
  /**
   * When worker passes customPiecesDir=[userDir, globalDir] to ToolContext,
   * ListPieces and GetPiece must see pieces from BOTH dirs.
   * CreatePiece must write to dirs[0] (per-user dir, not global).
   */
  let userDir: string;
  let globalDir: string;

  beforeEach(() => {
    userDir = mkdtempSync(join(tmpdir(), 'pieces-user-'));
    globalDir = mkdtempSync(join(tmpdir(), 'pieces-global-'));
  });

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

  const userPiece = `name: user-only-piece
description: user piece
max_movements: 1
initial_movement: do
movements:
  - name: do
    edit: false
    persona: p
    instruction: i
    allowed_tools: []
    rules:
      - condition: done
        next: do
`;

  const globalPiece = `name: global-only-piece
description: global piece
max_movements: 1
initial_movement: do
movements:
  - name: do
    edit: false
    persona: p
    instruction: i
    allowed_tools: []
    rules:
      - condition: done
        next: do
`;

  function arrayCtx(): ToolContext {
    return { workspacePath: '/tmp/dummy', editAllowed: false, customPiecesDir: [userDir, globalDir] };
  }

  it('ListPieces returns pieces from BOTH per-user and global-custom dirs', async () => {
    writeFileSync(join(userDir, 'user-only-piece.yaml'), userPiece, 'utf-8');
    writeFileSync(join(globalDir, 'global-only-piece.yaml'), globalPiece, 'utf-8');

    const result = await executeTool('ListPieces', {}, arrayCtx());
    expect(result?.isError).toBe(false);
    expect(result?.output).toContain('user-only-piece');
    expect(result?.output).toContain('global-only-piece');
  });

  it('GetPiece finds a piece in the second custom dir (global-custom)', async () => {
    writeFileSync(join(globalDir, 'global-only-piece.yaml'), globalPiece, 'utf-8');

    const result = await executeTool('GetPiece', { name: 'global-only-piece' }, arrayCtx());
    expect(result?.isError).toBe(false);
    expect(result?.output).toContain('global piece');
  });

  it('GetPiece prefers dirs[0] (per-user) over dirs[1] (global) for same name', async () => {
    const userVersion = userPiece.replace('description: user piece', 'description: per-user version');
    writeFileSync(join(userDir, 'global-only-piece.yaml'), userVersion, 'utf-8');
    writeFileSync(join(globalDir, 'global-only-piece.yaml'), globalPiece, 'utf-8');

    const result = await executeTool('GetPiece', { name: 'global-only-piece' }, arrayCtx());
    expect(result?.isError).toBe(false);
    expect(result?.output).toContain('per-user version');
  });

  it('CreatePiece writes to dirs[0] (per-user dir), not global dir', async () => {
    const newPiece = `name: brand-new-piece
description: new
max_movements: 5
initial_movement: do
movements:
  - name: do
    edit: false
    persona: p
    instruction: i
    allowed_tools: []
    rules:
      - condition: done
        next: do
`;
    const result = await executeTool('CreatePiece', { yaml_content: newPiece }, arrayCtx());
    expect(result?.isError).toBe(false);
    // Must land in userDir (dirs[0])
    expect(existsSync(join(userDir, 'brand-new-piece.yaml'))).toBe(true);
    // Must NOT land in globalDir (dirs[1])
    expect(existsSync(join(globalDir, 'brand-new-piece.yaml'))).toBe(false);
  });
});

describe('UpdatePiece', () => {
  // Uses the real bundled `chat.yaml` to exercise the built-in guard. The
  // file's pre-update content is captured up-front so we can detect any
  // accidental write.
  const BUILTIN_NAME = 'chat';
  const builtinPath = join(process.cwd(), 'pieces', `${BUILTIN_NAME}.yaml`);
  const originalBuiltin = readFileSync(builtinPath, 'utf-8');

  afterEach(() => {
    // Defense in depth: if a future regression lets the built-in be
    // overwritten, this restores it so the rest of the suite isn't poisoned.
    writeFileSync(builtinPath, originalBuiltin, 'utf-8');
  });

  it('refuses to overwrite a built-in piece (regression: agent corrupted game-tweet-generator)', async () => {
    const malicious = VALID_NEW_PIECE_YAML.replace('test-fresh-piece', BUILTIN_NAME);
    const result = await executeTool(
      'UpdatePiece',
      { name: BUILTIN_NAME, yaml_content: malicious },
      ctx(),
    );
    expect(result?.isError).toBe(true);
    expect(result?.output).toMatch(/組み込み|built-in/);
    // Built-in file must remain untouched.
    expect(readFileSync(builtinPath, 'utf-8')).toBe(originalBuiltin);
  });

  it('allows update when a custom override already exists', async () => {
    const customPath = join(customDir, `${BUILTIN_NAME}.yaml`);
    cpSync(builtinPath, customPath);
    const updated = VALID_NEW_PIECE_YAML.replace('test-fresh-piece', BUILTIN_NAME);
    const result = await executeTool(
      'UpdatePiece',
      { name: BUILTIN_NAME, yaml_content: updated },
      ctx(),
    );
    expect(result?.isError).toBe(false);
    // Custom override took the write; built-in stays clean.
    expect(readFileSync(customPath, 'utf-8')).toContain('description: example');
    expect(readFileSync(builtinPath, 'utf-8')).toBe(originalBuiltin);
  });
});
