import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import Database from 'better-sqlite3';
import { TOOL_DEFS, executeTool, resolveAppDocPath, setAppDocsDeps } from './app-docs.js';
import type { ToolContext } from './core.js';

const baseCtx: ToolContext = {
  workspacePath: '/tmp/app-docs-test',
  editAllowed: false,
};

describe('app-docs: TOOL_DEFS', () => {
  it('exposes ReadAppDoc, ListAppDocs, GetMyOrchestratorState', () => {
    expect(TOOL_DEFS).toHaveProperty('ReadAppDoc');
    expect(TOOL_DEFS).toHaveProperty('ListAppDocs');
    expect(TOOL_DEFS).toHaveProperty('GetMyOrchestratorState');
  });
});

describe('app-docs: resolveAppDocPath (path safety)', () => {
  it('rejects claude-md (internal doc)', () => {
    expect(resolveAppDocPath('claude-md')).toBeNull();
  });

  it('rejects CLAUDE.md (internal doc)', () => {
    expect(resolveAppDocPath('CLAUDE.md')).toBeNull();
  });

  it('rejects agents-md (internal doc)', () => {
    expect(resolveAppDocPath('agents-md')).toBeNull();
  });

  it('rejects AGENTS.md (internal doc)', () => {
    expect(resolveAppDocPath('AGENTS.md')).toBeNull();
  });

  it('rejects readme / README.md (internal doc)', () => {
    expect(resolveAppDocPath('readme')).toBeNull();
    expect(resolveAppDocPath('README.md')).toBeNull();
  });

  it('rejects docs/superpowers/* (internal implementation plans)', () => {
    expect(resolveAppDocPath('docs/superpowers/plans/foo')).toBeNull();
    expect(resolveAppDocPath('docs/superpowers/anything')).toBeNull();
  });

  it('rejects docs/maintenance-checklist (internal ops reference)', () => {
    expect(resolveAppDocPath('docs/maintenance-checklist')).toBeNull();
    expect(resolveAppDocPath('docs/maintenance-checklist.md')).toBeNull();
  });

  it('still resolves docs/mcp (allowed user-facing doc)', () => {
    const r = resolveAppDocPath('docs/mcp');
    expect(r).not.toBeNull();
    expect(r!.label).toBe('docs/mcp');
    expect(r!.path).toMatch(/docs\/mcp\.md$/);
  });

  it('resolves piece/<name>', () => {
    const r = resolveAppDocPath('piece/chat');
    expect(r).not.toBeNull();
    expect(r!.label).toBe('pieces/chat.yaml');
    expect(r!.path).toMatch(/pieces\/chat\.yaml$/);
  });

  it('resolves docs/<path> with auto .md suffix', () => {
    const r = resolveAppDocPath('docs/architecture');
    expect(r).not.toBeNull();
    expect(r!.label).toBe('docs/architecture');
    expect(r!.path).toMatch(/docs\/architecture\.md$/);
  });

  it('resolves docs/<path>.md without doubling extension', () => {
    const r = resolveAppDocPath('docs/architecture.md');
    expect(r).not.toBeNull();
    expect(r!.path).toMatch(/docs\/architecture\.md$/);
    expect(r!.path).not.toMatch(/\.md\.md$/);
  });

  it('resolves tool/<name> (lowercased)', () => {
    const r = resolveAppDocPath('tool/BrowseWeb');
    expect(r).not.toBeNull();
    expect(r!.label).toBe('docs/tools/browseweb.md');
  });

  it('rejects path traversal via ..', () => {
    expect(resolveAppDocPath('../etc/passwd')).toBeNull();
    expect(resolveAppDocPath('docs/../../etc/passwd')).toBeNull();
    expect(resolveAppDocPath('piece/../secret')).toBeNull();
  });

  it('rejects names with invalid characters', () => {
    expect(resolveAppDocPath('piece/with space')).toBeNull();
    expect(resolveAppDocPath('piece/$shell')).toBeNull();
    expect(resolveAppDocPath('docs/with;semicolon')).toBeNull();
  });

  it('rejects unknown top-level names', () => {
    expect(resolveAppDocPath('random-name')).toBeNull();
    expect(resolveAppDocPath('config-yaml')).toBeNull();
  });

  it('rejects empty / non-string', () => {
    expect(resolveAppDocPath('')).toBeNull();
    expect(resolveAppDocPath(null as unknown as string)).toBeNull();
    expect(resolveAppDocPath(undefined as unknown as string)).toBeNull();
  });
});

describe('app-docs: ReadAppDoc execution', () => {
  it('rejects claude-md with an error (internal doc blocked)', async () => {
    const res = await executeTool('ReadAppDoc', { name: 'claude-md' }, baseCtx);
    expect(res).not.toBeNull();
    expect(res!.isError).toBe(true);
    expect(res!.output).toContain('不正な name');
  });

  it('returns error with hint when name is missing', async () => {
    const res = await executeTool('ReadAppDoc', {}, baseCtx);
    expect(res!.isError).toBe(true);
    expect(res!.output).toContain('name パラメータ');
  });

  it('returns error with hint when name is invalid', async () => {
    const res = await executeTool('ReadAppDoc', { name: 'bogus name with space' }, baseCtx);
    expect(res!.isError).toBe(true);
    expect(res!.output).toContain('不正な name');
    expect(res!.output).toContain('ListAppDocs');
  });

  it('returns error when name resolves but file does not exist', async () => {
    const res = await executeTool('ReadAppDoc', { name: 'docs/no-such-doc' }, baseCtx);
    expect(res!.isError).toBe(true);
    expect(res!.output).toContain('存在しません');
  });

  it('reads an existing piece YAML', async () => {
    const res = await executeTool('ReadAppDoc', { name: 'piece/chat' }, baseCtx);
    expect(res!.isError).toBe(false);
    expect(res!.output).toContain('pieces/chat.yaml');
    expect(res!.output).toContain('name: chat');
  });

  it('returns null for unrelated tool name', async () => {
    const res = await executeTool('SomeOtherTool', {}, baseCtx);
    expect(res).toBeNull();
  });
});

describe('app-docs: ListAppDocs', () => {
  it('groups output into piece / docs / tools sections (no project overview)', async () => {
    const res = await executeTool('ListAppDocs', {}, baseCtx);
    expect(res!.isError).toBe(false);
    const out = res!.output;
    // Removed section
    expect(out).not.toContain('# プロジェクト概要');
    // Remaining sections
    expect(out).toContain('# Piece 一覧');
    expect(out).toContain('# ドキュメント');
    expect(out).toContain('# ツール参照');
    // Should mention at least one known piece
    expect(out).toContain('piece/chat');
  });

  it('does not list CLAUDE.md / AGENTS.md / README.md', async () => {
    const res = await executeTool('ListAppDocs', {}, baseCtx);
    const out = res!.output;
    expect(out).not.toContain('claude-md');
    expect(out).not.toContain('agents-md');
    expect(out).not.toContain('readme');
    expect(out).not.toContain('CLAUDE.md');
    expect(out).not.toContain('AGENTS.md');
  });

  it('does not list docs/superpowers or docs/maintenance-checklist', async () => {
    const res = await executeTool('ListAppDocs', {}, baseCtx);
    const out = res!.output;
    expect(out).not.toContain('superpowers');
    expect(out).not.toContain('maintenance-checklist');
  });
});

describe('app-docs: GetMyOrchestratorState', () => {
  let tmpDir: string;
  let db: Database.Database;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'app-docs-state-'));
    db = new Database(':memory:');
    // Minimal schema: just the columns we read
    db.exec(`
      CREATE TABLE users (
        id TEXT PRIMARY KEY,
        email TEXT,
        name TEXT,
        role TEXT NOT NULL DEFAULT 'user',
        status TEXT NOT NULL DEFAULT 'active'
      );
      CREATE TABLE local_tasks (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT,
        piece_name TEXT,
        owner_id TEXT,
        state TEXT,
        created_at TEXT
      );
      CREATE TABLE jobs (
        id TEXT PRIMARY KEY,
        repo TEXT,
        issue_number INTEGER,
        status TEXT,
        created_at TEXT
      );
      CREATE TABLE mcp_servers (
        id TEXT PRIMARY KEY,
        name TEXT,
        auth_kind TEXT,
        owner_id TEXT,
        enabled INTEGER
      );
      CREATE TABLE user_mcp_tokens (
        user_id TEXT,
        server_id TEXT,
        expires_at TEXT
      );
    `);

    db.prepare('INSERT INTO users (id, email, name, role) VALUES (?, ?, ?, ?)').run('alice', 'a@example.com', 'Alice', 'admin');

    db.prepare('INSERT INTO local_tasks (title, piece_name, owner_id, state, created_at) VALUES (?, ?, ?, ?, ?)')
      .run('Hello task', 'chat', 'alice', 'open', '2026-05-10 09:00:00');
    db.prepare('INSERT INTO local_tasks (title, piece_name, owner_id, state, created_at) VALUES (?, ?, ?, ?, ?)')
      .run('Other task', 'research', 'alice', 'open', '2026-05-09 09:00:00');

    db.prepare('INSERT INTO mcp_servers (id, name, auth_kind, owner_id, enabled) VALUES (?, ?, ?, ?, ?)')
      .run('canva', 'Canva', 'oauth', null, 1);
    db.prepare('INSERT INTO mcp_servers (id, name, auth_kind, owner_id, enabled) VALUES (?, ?, ?, ?, ?)')
      .run('tundoc', 'Tundoc', 'api_key', 'alice', 1);

    db.prepare('INSERT INTO user_mcp_tokens (user_id, server_id, expires_at) VALUES (?, ?, ?)')
      .run('alice', 'canva', '2027-01-01 00:00:00');

    setAppDocsDeps({ db, userFolderRoot: tmpDir });
  });

  afterEach(() => {
    db.close();
    rmSync(tmpDir, { recursive: true, force: true });
    setAppDocsDeps(null);
  });

  it('requires userId in ctx', async () => {
    const res = await executeTool('GetMyOrchestratorState', {}, baseCtx);
    expect(res!.isError).toBe(true);
    expect(res!.output).toContain('authenticated user');
  });

  it('returns sections covering user / tasks / MCP / user folder', async () => {
    const ctx: ToolContext = { ...baseCtx, userId: 'alice' };
    const res = await executeTool('GetMyOrchestratorState', {}, ctx);
    expect(res!.isError).toBe(false);
    const out = res!.output;
    expect(out).toContain('## ユーザー');
    expect(out).toContain('Alice');
    expect(out).toContain('## 最近のタスク');
    expect(out).toContain('Hello task');
    expect(out).toContain('## MCP サーバー');
    expect(out).toContain('canva');
    expect(out).toContain('連携済み');
    expect(out).toContain('tundoc');
    expect(out).toContain('api_key');
    expect(out).toContain('## ユーザーフォルダ');
  });

  it('handles empty memory/scripts dirs cleanly', async () => {
    // Build empty user folder
    mkdirSync(join(tmpDir, 'alice'));
    mkdirSync(join(tmpDir, 'alice', 'memory'));
    mkdirSync(join(tmpDir, 'alice', 'scripts'));
    writeFileSync(join(tmpDir, 'alice', 'AGENTS.md'), 'hello');

    const ctx: ToolContext = { ...baseCtx, userId: 'alice' };
    const res = await executeTool('GetMyOrchestratorState', {}, ctx);
    expect(res!.isError).toBe(false);
    expect(res!.output).toContain('AGENTS.md: 5 bytes');
    expect(res!.output).toContain('memory/: 0 件');
    expect(res!.output).toContain('scripts/: 0 件');
  });

  it('does not leak OAuth secrets or tokens in output', async () => {
    const ctx: ToolContext = { ...baseCtx, userId: 'alice' };
    const res = await executeTool('GetMyOrchestratorState', {}, ctx);
    const out = res!.output;
    // Should NOT contain any of the encrypted blob / token columns
    expect(out).not.toMatch(/oauth_client_secret/);
    expect(out).not.toMatch(/static_token/);
    expect(out).not.toMatch(/access_token/);
  });
});
