local createLibTest = require('/apis/libtest');
local createMcpComputer = require('/apis/libmcpcomputer');

local testlib = createLibTest({ ... });

local function fakeOs(id, label)
  return {
    getComputerID = function() return id; end,
    getComputerLabel = function() return label; end,
  };
end

local function fakeFs()
  local files = {};
  return {
    files = files,
    open = function(path, mode)
      if mode == 'r' then
        if files[path] == nil then
          return nil, 'No such file';
        end
        return {
          readAll = function()
            return files[path];
          end,
          close = function() end,
        };
      end

      if mode ~= 'w' then
        return nil, 'unsupported mode';
      end
      if string.find(path, 'missing-parent', 1, true) then
        return nil, 'No such file';
      end
      local buffer = {};
      return {
        write = function(value)
          buffer[#buffer + 1] = value;
        end,
        close = function()
          files[path] = table.concat(buffer);
        end,
      };
    end,
  };
end

local function fsWithFiles(files)
  local fsLike = fakeFs();
  for path, content in pairs(files) do
    fsLike.files[path] = content;
  end
  return fsLike;
end

testlib.test('formatPong includes computer id and label', function()
  local mcpComputer = createMcpComputer();

  testlib.assertEquals(mcpComputer.formatPong(fakeOs(12, 'base-turtle')), 'pong from 12 (Label: base-turtle)');
end);

testlib.test('formatPong renders missing or empty labels as null', function()
  local mcpComputer = createMcpComputer();

  testlib.assertEquals(mcpComputer.formatPong(fakeOs(7, nil)), 'pong from 7 (Label: null)');
  testlib.assertEquals(mcpComputer.formatPong(fakeOs(8, '')), 'pong from 8 (Label: null)');
end);

testlib.test('executeLua captures output and return values', function()
  local mcpComputer = createMcpComputer();
  local result = mcpComputer.executeLua("print('hello', 'world'); write('tail'); return 2 + 3, nil, 'ok'");

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.output, 'hello\tworld\ntail');
  testlib.assertEquals(result.returns[1].type, 'number');
  testlib.assertEquals(result.returns[1].value, 5);
  testlib.assertEquals(result.returns[2].type, 'nil');
  testlib.assertEquals(result.returns[3].type, 'string');
  testlib.assertEquals(result.returns[3].value, 'ok');
end);

testlib.test('executeLua reports runtime errors with captured output', function()
  local mcpComputer = createMcpComputer();
  local result = mcpComputer.executeLua("print('before'); error('boom', 0)");

  testlib.assertEquals(result.ok, false);
  testlib.assertEquals(result.output, 'before\n');
  testlib.assertTrue(string.find(result.error, 'boom', 1, true));
end);

testlib.test('executeLua reports syntax errors', function()
  local mcpComputer = createMcpComputer();
  local result = mcpComputer.executeLua("print('unterminated'\nreturn 1");

  testlib.assertEquals(result.ok, false);
  testlib.assertEquals(result.output, '');
  testlib.assertTrue(string.find(result.error, 'expected', 1, true)
    or string.find(result.error, 'near', 1, true));
end);

testlib.test('executeLua serializes non-json return values as descriptors', function()
  local mcpComputer = createMcpComputer();
  local result = mcpComputer.executeLua('return { answer = 42 }, function() end, coroutine.create(function() end)');

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].type, 'table');
  testlib.assertTrue(type(result.returns[1].repr) == 'string');
  testlib.assertEquals(result.returns[2].type, 'function');
  testlib.assertTrue(type(result.returns[2].repr) == 'string');
  testlib.assertEquals(result.returns[3].type, 'thread');
  testlib.assertTrue(type(result.returns[3].repr) == 'string');
end);

testlib.test('executeLua leaves direct terminal writes out of captured output', function()
  local mcpComputer = createMcpComputer();
  local result = mcpComputer.executeLua("term.write('visible'); return 'done'");

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.output, '');
  testlib.assertEquals(result.returns[1].type, 'string');
  testlib.assertEquals(result.returns[1].value, 'done');
end);

testlib.test('executeLua injects require and package into the environment', function()
  local seenEnv, seenDir;
  local mcpComputer = createMcpComputer({
    requireFactory = function(env, dir)
      seenEnv, seenDir = env, dir;
      return function() return 'REQUIRED'; end, { loaded = {} };
    end,
  });

  local result = mcpComputer.executeLua("return require('whatever'), type(package)");

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].type, 'string');
  testlib.assertEquals(result.returns[1].value, 'REQUIRED');
  testlib.assertEquals(result.returns[2].type, 'string');
  testlib.assertEquals(result.returns[2].value, 'table');
  testlib.assertEquals(type(seenEnv), 'table');
  testlib.assertEquals(seenDir, '');
end);

testlib.test('executeLua passes the configured requireDir to the factory', function()
  local seenDir;
  local mcpComputer = createMcpComputer({
    requireDir = '/apis',
    requireFactory = function(_, dir)
      seenDir = dir;
      return function() end, {};
    end,
  });

  mcpComputer.executeLua('return 1');

  testlib.assertEquals(seenDir, '/apis');
end);

testlib.test('executeLua leaves require unset when injection is disabled', function()
  local mcpComputer = createMcpComputer({ requireFactory = false });

  local result = mcpComputer.executeLua('return require == nil, package == nil');

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].type, 'boolean');
  testlib.assertEquals(result.returns[1].value, true);
  testlib.assertEquals(result.returns[2].type, 'boolean');
  testlib.assertEquals(result.returns[2].value, true);
end);

testlib.test('executeFile injects require into the file environment', function()
  local mcpComputer = createMcpComputer({
    requireFactory = function() return function() return 'FROM_FILE'; end, {}; end,
  });
  local fsLike = fsWithFiles({
    ['/needs-require.lua'] = "return require('x')",
  });

  local result = mcpComputer.executeFile('/needs-require.lua', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].type, 'string');
  testlib.assertEquals(result.returns[1].value, 'FROM_FILE');
end);

testlib.test('executeFile passes the file directory to require factory', function()
  local seenDir;
  local mcpComputer = createMcpComputer({
    requireFactory = function(_, dir)
      seenDir = dir;
      return function() return 'ok'; end, {};
    end,
  });
  local fsLike = fsWithFiles({
    ['/programs/tool.lua'] = "return require('dep')",
  });

  local result = mcpComputer.executeFile('/programs/tool.lua', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(seenDir, '/programs');
end);

testlib.test('executeFile captures output and return values from file', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/helloworld.lua'] = "print('hello', 'file'); write('tail'); return 42, 'ok'",
  });

  local result = mcpComputer.executeFile('/helloworld.lua', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.output, 'hello\tfile\ntail');
  testlib.assertEquals(result.returns[1].type, 'number');
  testlib.assertEquals(result.returns[1].value, 42);
  testlib.assertEquals(result.returns[2].type, 'string');
  testlib.assertEquals(result.returns[2].value, 'ok');
end);

testlib.test('executeFile passes args as varargs and arg table', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/argv.lua'] = "local a, b = ...; return a, b, arg[1], arg[2]",
  });

  local result = mcpComputer.executeFile('/argv.lua', fsLike, { 'install', 'pkg' });

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].value, 'install');
  testlib.assertEquals(result.returns[2].value, 'pkg');
  testlib.assertEquals(result.returns[3].value, 'install');
  testlib.assertEquals(result.returns[4].value, 'pkg');
end);

testlib.test('executeFile validates args', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/argv.lua'] = 'return true',
  });

  local result = mcpComputer.executeFile('/argv.lua', fsLike, { 'ok', 2 });

  testlib.assertEquals(result.ok, false);
  testlib.assertTrue(string.find(result.error, 'args', 1, true));
end);

testlib.test('executeFile provides shell getRunningProgram for the file', function()
  local mcpComputer = createMcpComputer({ shell = { path = function() return '/rom/programs'; end } });
  local fsLike = fsWithFiles({
    ['/programs/tool.lua'] = "return shell.getRunningProgram(), shell.path()",
  });

  local result = mcpComputer.executeFile('/programs/tool.lua', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.returns[1].value, '/programs/tool.lua');
  testlib.assertEquals(result.returns[2].value, '/rom/programs');
end);

testlib.test('executeFile reports validation and missing file errors', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fakeFs();

  local missingPath = mcpComputer.executeFile('', fsLike);
  local missingFile = mcpComputer.executeFile('/missing.lua', fsLike);

  testlib.assertEquals(missingPath.ok, false);
  testlib.assertTrue(string.find(missingPath.error, 'path', 1, true));
  testlib.assertEquals(missingPath.output, '');
  testlib.assertEquals(missingFile.ok, false);
  testlib.assertTrue(string.find(missingFile.error, 'No such file', 1, true));
  testlib.assertEquals(missingFile.output, '');
end);

testlib.test('executeFile reports syntax errors with file chunk name', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/broken.lua'] = "print('unterminated'\nreturn 1",
  });

  local result = mcpComputer.executeFile('/broken.lua', fsLike);

  testlib.assertEquals(result.ok, false);
  testlib.assertEquals(result.output, '');
  testlib.assertTrue(string.find(result.error, '/broken.lua', 1, true));
end);

testlib.test('executeFile reports runtime errors with captured output', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/boom.lua'] = "print('before'); error('boom', 0)",
  });

  local result = mcpComputer.executeFile('/boom.lua', fsLike);

  testlib.assertEquals(result.ok, false);
  testlib.assertEquals(result.output, 'before\n');
  testlib.assertTrue(string.find(result.error, 'boom', 1, true));
end);

testlib.test('executeFile leaves direct terminal writes out of captured output', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fsWithFiles({
    ['/screen.lua'] = "term.write('visible'); return 'done'",
  });

  local result = mcpComputer.executeFile('/screen.lua', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.output, '');
  testlib.assertEquals(result.returns[1].type, 'string');
  testlib.assertEquals(result.returns[1].value, 'done');
end);

testlib.test('writeFile writes and overwrites file content', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fakeFs();

  local first = mcpComputer.writeFile('/tmp/note.txt', 'hello', fsLike);
  local second = mcpComputer.writeFile('/tmp/note.txt', 'goodbye', fsLike);

  testlib.assertEquals(first.ok, true);
  testlib.assertEquals(first.path, '/tmp/note.txt');
  testlib.assertEquals(first.bytes, 5);
  testlib.assertEquals(second.ok, true);
  testlib.assertEquals(second.bytes, 7);
  testlib.assertEquals(fsLike.files['/tmp/note.txt'], 'goodbye');
end);

testlib.test('writeFile allows empty content', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fakeFs();
  local result = mcpComputer.writeFile('/tmp/empty.txt', '', fsLike);

  testlib.assertEquals(result.ok, true);
  testlib.assertEquals(result.bytes, 0);
  testlib.assertEquals(fsLike.files['/tmp/empty.txt'], '');
end);

testlib.test('writeFile validates path and content', function()
  local mcpComputer = createMcpComputer();
  local fsLike = fakeFs();

  local missingPath = mcpComputer.writeFile('', 'hello', fsLike);
  local invalidContent = mcpComputer.writeFile('/tmp/note.txt', 12, fsLike);
  local missingParent = mcpComputer.writeFile('/missing-parent/note.txt', 'hello', fsLike);

  testlib.assertEquals(missingPath.ok, false);
  testlib.assertTrue(string.find(missingPath.error, 'path', 1, true));
  testlib.assertEquals(invalidContent.ok, false);
  testlib.assertTrue(string.find(invalidContent.error, 'content', 1, true));
  testlib.assertEquals(missingParent.ok, false);
  testlib.assertTrue(string.find(missingParent.error, 'No such file', 1, true));
end);

testlib.run();
