'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const assert = require('assert');
const { execSync } = require('child_process');

const GEN_CONTEXT = path.resolve(__dirname, '../../../gen-context.js');

let passed = 0;
let failed = 0;

function test(name, fn) {
  try {
    fn();
    console.log(`  PASS  ${name}`);
    passed++;
  } catch (err) {
    console.log(`  FAIL  ${name}: ${err.message}`);
    failed++;
  }
}

/**
 * Send one or more newline-separated JSON-RPC messages to the MCP server
 * via stdin, collect stdout, return array of parsed response objects.
 */
function mcpCall(messages, cwd) {
  const input = (Array.isArray(messages) ? messages : [messages])
    .map((m) => JSON.stringify(m))
    .join('\n') + '\n';

  const stdout = execSync(`node "${GEN_CONTEXT}" --mcp`, {
    input,
    cwd,
    encoding: 'utf8',
    timeout: 10000,
  });

  return stdout
    .split('\n')
    .filter((l) => l.trim())
    .map((l) => JSON.parse(l));
}

function withTempProject(fn) {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-mcp-'));
  try {
    fn(dir);
  } finally {
    fs.rmSync(dir, { recursive: true, force: true });
  }
}

function seedContextFile(dir) {
  const ghDir = path.join(dir, '.github');
  fs.mkdirSync(ghDir, { recursive: true });
  fs.writeFileSync(
    path.join(ghDir, 'copilot-instructions.md'),
    [
      '<!-- Generated by SigMap -->',
      '# Code signatures',
      '',
      '## src',
      '',
      '### src/auth/login.js',
      '```',
      'function login(user, pass)',
      'function logout(session)',
      '```',
      '',
      '### src/api/routes.js',
      '```',
      'function getUser(req, res)',
      'function createUser(req, res)',
      '```',
      '',
    ].join('\n')
  );
}

// ─────────────────────────────────────────────────────────────
// Gate 1: initialize handshake
// ─────────────────────────────────────────────────────────────
test('initialize returns serverInfo', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'initialize', id: 1, params: { protocolVersion: '2024-11-05', capabilities: {} } },
      dir
    );
    assert.strictEqual(res.jsonrpc, '2.0');
    assert.strictEqual(res.id, 1);
    assert.ok(res.result, 'Should have result');
    assert.strictEqual(res.result.serverInfo.name, 'sigmap');
    assert.ok(res.result.protocolVersion, 'Should have protocolVersion');
    assert.ok(res.result.capabilities, 'Should have capabilities');
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 2: tools/list returns 12 tools
// ─────────────────────────────────────────────────────────────
test('tools/list returns exactly 18 tools', () => {
  withTempProject((dir) => {
    const [res] = mcpCall({ jsonrpc: '2.0', method: 'tools/list', id: 2 }, dir);
    assert.ok(res.result, 'Should have result');
    assert.ok(Array.isArray(res.result.tools), 'tools should be array');
    assert.strictEqual(res.result.tools.length, 18);
    const names = res.result.tools.map((t) => t.name);
    assert.ok(names.includes('read_context'), 'Should have read_context');
    assert.ok(names.includes('search_signatures'), 'Should have search_signatures');
    assert.ok(names.includes('get_map'), 'Should have get_map');
    assert.ok(names.includes('create_checkpoint'), 'Should have create_checkpoint');
    assert.ok(names.includes('get_routing'), 'Should have get_routing');
    assert.ok(names.includes('explain_file'), 'Should have explain_file');
    assert.ok(names.includes('list_modules'), 'Should have list_modules');
    assert.ok(names.includes('query_context'), 'Should have query_context');
    assert.ok(names.includes('get_impact'), 'Should have get_impact');
    assert.ok(names.includes('get_lines'), 'Should have get_lines');
    assert.ok(names.includes('read_memory'), 'Should have read_memory');
    assert.ok(names.includes('get_callee_signatures'), 'Should have get_callee_signatures');
    assert.ok(names.includes('sigmap_notify_file_created'), 'Should have notify_file_created');
    assert.ok(names.includes('sigmap_notify_symbol_added'), 'Should have notify_symbol_added');
    assert.ok(names.includes('sigmap_notify_file_deleted'), 'Should have notify_file_deleted');
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 2b: get_callee_signatures resolves known + unknown symbols (bundle path)
// ─────────────────────────────────────────────────────────────
test('get_callee_signatures returns exact signature for a known symbol', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 22,
        params: { name: 'get_callee_signatures', arguments: { symbols: ['login', 'nope_xyz'] } } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(text.includes('login(user, pass)'), 'exact signature for login');
    assert.ok(/nope_xyz[\s\S]*not found/.test(text), 'unknown symbol marked not found');
  });
});

test('get_callee_signatures suggests a closest match for a near-miss', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 23,
        params: { name: 'get_callee_signatures', arguments: { symbols: ['getUsr'] } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(/Did you mean .*getUser/.test(text), `expected getUser suggestion, got: ${text}`);
  });
});

test('get_callee_signatures errors on missing symbols arg', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 24,
        params: { name: 'get_callee_signatures', arguments: {} } },
      dir
    );
    assert.ok(/Missing required argument: symbols/.test(res.result.content[0].text), 'should report missing arg');
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 2bb: read-time self-heal — disk edits reflected without a hook
// ─────────────────────────────────────────────────────────────
test('self-heal: a file written on disk (no hook) is resolvable on read', () => {
  withTempProject((dir) => {
    seedContextFile(dir); // gives a context-file mtime baseline
    const f = path.join(dir, 'src', 'healed.js');
    fs.mkdirSync(path.dirname(f), { recursive: true });
    fs.writeFileSync(f, 'function selfHealed(a, b){ return a + b; }\nmodule.exports = { selfHealed };\n');
    const future = Date.now() / 1000 + 30;
    fs.utimesSync(f, future, future); // ensure mtime > context, defeat ms granularity
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 30,
        params: { name: 'get_callee_signatures', arguments: { symbols: ['selfHealed'] } } },
      dir
    );
    assert.ok(/selfHealed\(a, b\)/.test(res.result.content[0].text),
      `disk edit should self-heal into the index: ${res.result.content[0].text}`);
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 2c: Layer 1 write hooks — live index for agent-created code
// ─────────────────────────────────────────────────────────────
test('notify_file_created makes a new symbol live (same session)', () => {
  withTempProject((dir) => {
    const res = mcpCall([
      { jsonrpc: '2.0', method: 'tools/call', id: 1,
        params: { name: 'sigmap_notify_file_created',
          arguments: { path: 'src/rate.js', content: 'function rateLimit(key, max){ return max; }\nmodule.exports = { rateLimit };\n' } } },
      { jsonrpc: '2.0', method: 'tools/call', id: 2,
        params: { name: 'get_callee_signatures', arguments: { symbols: ['rateLimit'] } } },
    ], dir);
    const created = res.find((r) => r.id === 1).result.content[0].text;
    const callee = res.find((r) => r.id === 2).result.content[0].text;
    assert.ok(/Indexed/.test(created), `created: ${created}`);
    assert.ok(/rateLimit\(key, max\)/.test(callee), `callee should resolve: ${callee}`);
  });
});

test('notify_symbol_added registers a single signature', () => {
  withTempProject((dir) => {
    const res = mcpCall([
      { jsonrpc: '2.0', method: 'tools/call', id: 1,
        params: { name: 'sigmap_notify_symbol_added',
          arguments: { signature: 'function quickHash(s)', file: 'src/util.js', line: 7 } } },
      { jsonrpc: '2.0', method: 'tools/call', id: 2,
        params: { name: 'search_signatures', arguments: { query: 'quickHash' } } },
    ], dir);
    assert.ok(/quickHash/.test(res.find((r) => r.id === 2).result.content[0].text), 'symbol should be searchable');
  });
});

test('notify_file_deleted removes a previously-created file', () => {
  withTempProject((dir) => {
    const res = mcpCall([
      { jsonrpc: '2.0', method: 'tools/call', id: 1,
        params: { name: 'sigmap_notify_file_created',
          arguments: { path: 'src/gone.js', content: 'function willVanish(){ return 1; }\n' } } },
      { jsonrpc: '2.0', method: 'tools/call', id: 2,
        params: { name: 'sigmap_notify_file_deleted', arguments: { path: 'src/gone.js' } } },
      { jsonrpc: '2.0', method: 'tools/call', id: 3,
        params: { name: 'get_callee_signatures', arguments: { symbols: ['willVanish'] } } },
    ], dir);
    assert.ok(/Removed/.test(res.find((r) => r.id === 2).result.content[0].text), 'should report removal');
    const after = res.find((r) => r.id === 3).result.content[0].text;
    assert.ok(!/willVanish\(/.test(after), `symbol should be gone: ${after}`);
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 3: read_context returns signatures
// ─────────────────────────────────────────────────────────────
test('read_context returns full context file content', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 3, params: { name: 'read_context', arguments: {} } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(text.includes('login'), 'Should include login signature');
    assert.ok(text.includes('getUser'), 'Should include getUser signature');
  });
});

// ─────────────────────────────────────────────────────────────
// Gate 4: read_context with module filter
// ─────────────────────────────────────────────────────────────
test('read_context with module filters to matching files', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 4, params: { name: 'read_context', arguments: { module: 'src/auth' } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('login'), 'Should include auth signatures');
    assert.ok(!text.includes('getUser'), 'Should NOT include api signatures');
  });
});

// ─────────────────────────────────────────────────────────────
// search_signatures
// ─────────────────────────────────────────────────────────────
test('search_signatures returns matching lines', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 5, params: { name: 'search_signatures', arguments: { query: 'user' } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('createUser') || text.includes('getUser'), 'Should find user-related signatures');
  });
});

test('search_signatures returns no-match message when nothing found', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 6, params: { name: 'search_signatures', arguments: { query: 'xyzzy_nonexistent_12345' } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('No signatures found'), 'Should return no-match message');
  });
});

// ─────────────────────────────────────────────────────────────
// get_map with missing PROJECT_MAP.md
// ─────────────────────────────────────────────────────────────
test('get_map returns error when PROJECT_MAP.md missing', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 7, params: { name: 'get_map', arguments: { type: 'imports' } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('PROJECT_MAP.md not found'), 'Should report missing file');
  });
});

// ─────────────────────────────────────────────────────────────
// Unknown tool → error response
// ─────────────────────────────────────────────────────────────
test('unknown tool returns error response', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 8, params: { name: 'nonexistent_tool', arguments: {} } },
      dir
    );
    assert.ok(res.error, 'Should return error object');
    assert.ok(res.error.message.includes('Unknown tool'), 'Error should mention Unknown tool');
  });
});

// ─────────────────────────────────────────────────────────────
// Unknown method → error response
// ─────────────────────────────────────────────────────────────
test('unknown method returns error response', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'resources/list', id: 9 },
      dir
    );
    assert.ok(res.error, 'Should return error object');
    assert.strictEqual(res.error.code, -32601);
  });
});

// ─────────────────────────────────────────────────────────────
// Multiple messages in one session
// ─────────────────────────────────────────────────────────────
test('multiple messages in a single session all get responses', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const responses = mcpCall(
      [
        { jsonrpc: '2.0', method: 'initialize', id: 1, params: { protocolVersion: '2024-11-05', capabilities: {} } },
        { jsonrpc: '2.0', method: 'tools/list', id: 2 },
        { jsonrpc: '2.0', method: 'tools/call', id: 3, params: { name: 'read_context', arguments: {} } },
      ],
      dir
    );
    assert.strictEqual(responses.length, 3, 'Should get 3 responses');
    assert.strictEqual(responses[0].id, 1);
    assert.strictEqual(responses[1].id, 2);
    assert.strictEqual(responses[2].id, 3);
  });
});

// ─────────────────────────────────────────────────────────────
// read_context when no context file exists
// ─────────────────────────────────────────────────────────────
test('read_context returns helpful message when no context file', () => {
  withTempProject((dir) => {
    // No seedContextFile — no .github/copilot-instructions.md
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 10, params: { name: 'read_context', arguments: {} } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('gen-context.js'), 'Should suggest running gen-context.js');
  });
});

// ─────────────────────────────────────────────────────────────
// create_checkpoint — v0.6
// ─────────────────────────────────────────────────────────────
test('create_checkpoint returns valid markdown', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 20, params: { name: 'create_checkpoint', arguments: {} } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(text.includes('# SigMap Checkpoint'), 'Should include checkpoint header');
    assert.ok(text.includes('## Git state'), 'Should include Git state section');
    assert.ok(text.includes('## Context snapshot'), 'Should include Context snapshot section');
    assert.ok(text.includes('**Created:**'), 'Should include timestamp');
  });
});

test('create_checkpoint includes note when provided', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 21, params: { name: 'create_checkpoint', arguments: { note: 'working on auth middleware' } } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('working on auth middleware'), 'Should include the note text');
    assert.ok(text.includes('**Note:**'), 'Should include Note label');
  });
});

test('create_checkpoint without note omits Note line', () => {
  withTempProject((dir) => {
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 22, params: { name: 'create_checkpoint', arguments: {} } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(!text.includes('**Note:**'), 'Should NOT include Note label when note is empty');
  });
});

test('create_checkpoint reports context stats when context file present', () => {
  withTempProject((dir) => {
    seedContextFile(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 23, params: { name: 'create_checkpoint', arguments: {} } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('**Token count:**'), 'Should show token count');
    assert.ok(text.includes('**Modules in context:**'), 'Should show module count');
  });
});

test('create_checkpoint reports missing context when no file', () => {
  withTempProject((dir) => {
    // No seedContextFile
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 24, params: { name: 'create_checkpoint', arguments: {} } },
      dir
    );
    const text = res.result.content[0].text;
    assert.ok(text.includes('gen-context.js'), 'Should suggest running gen-context.js');
  });
});

// ─────────────────────────────────────────────────────────────
// explain_file (imports/callers) and get_impact — extractImports export test
// ─────────────────────────────────────────────────────────────

function seedProjectWithImports(dir) {
  fs.mkdirSync(path.join(dir, 'src'), { recursive: true });

  fs.writeFileSync(path.join(dir, 'src', 'utils.js'), 'function log(msg) { console.log(msg); }');
  fs.writeFileSync(path.join(dir, 'src', 'config.js'), 'module.exports = { debug: true };');
  fs.writeFileSync(
    path.join(dir, 'src', 'app.js'),
    "const utils = require('./utils');\nconst config = require('./config');\nfunction start() { utils.log('started'); }"
  );
  fs.writeFileSync(path.join(dir, 'src', 'main.js'), "const app = require('./app');\napp.start();");

  // Generate context
  execSync(`node "${GEN_CONTEXT}"`, { cwd: dir, stdio: 'pipe' });
}

test('explain_file with imports parameter works without extractImports error', () => {
  withTempProject((dir) => {
    seedProjectWithImports(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 30, params: { name: 'explain_file', arguments: { path: 'src/app.js' } } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(!text.includes('extractImports is not a function'), 'Should NOT have extractImports error');
    assert.ok(text.includes('## Imports'), 'Should include Imports section');
  });
});

test('explain_file with callers parameter works without extractImports error', () => {
  withTempProject((dir) => {
    seedProjectWithImports(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 31, params: { name: 'explain_file', arguments: { path: 'src/utils.js' } } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(!text.includes('extractImports is not a function'), 'Should NOT have extractImports error');
    assert.ok(text.includes('## Callers'), 'Should include Callers section');
  });
});

test('get_impact tool works without extractImports error', () => {
  withTempProject((dir) => {
    seedProjectWithImports(dir);
    const [res] = mcpCall(
      { jsonrpc: '2.0', method: 'tools/call', id: 32, params: { name: 'get_impact', arguments: { file: 'src/utils.js' } } },
      dir
    );
    assert.ok(res.result, 'Should have result');
    const text = res.result.content[0].text;
    assert.ok(!text.includes('extractImports is not a function'), 'Should NOT have extractImports error');
    assert.ok(text.includes('## Impact'), 'Should include Impact section');
  });
});

console.log('');
console.log(`mcp-server: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
