import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { StdioTransport } from "./stdio-transport";
import type { McpRequest, McpResponse } from "./interfaces";
import { EventEmitter } from "node:events";

// Mock child_process
let mockProcess: any;

vi.mock("node:child_process", () => {
  return {
    spawn: vi.fn(() => {
      class MockChildProcess extends EventEmitter {
        stdin = {
          write: vi.fn((data: any, callback?: any) => {
            if (callback) callback();
            return true;
          }),
          end: vi.fn(),
        };
        stdout = new EventEmitter();
        stderr = new EventEmitter();
        kill = vi.fn(() => {
          this.killed = true;
          setTimeout(() => this.emit("exit", 0), 0);
        });
        killed = false;
        pid = 12345;
      }

      mockProcess = new MockChildProcess();
      return mockProcess;
    }),
  };
});

describe("StdioTransport", () => {
  let transport: StdioTransport;
  const command = "test-command";
  const args = ["arg1", "arg2"];
  const env = { TEST_VAR: "value" };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(async () => {
    if (transport && transport.isRunning()) {
      await transport.stop();
    }
  });

  describe("constructor", () => {
    it("should create transport with command only", () => {
      transport = new StdioTransport(command);
      expect(transport).toBeDefined();
      expect(transport.isRunning()).toBe(false);
    });

    it("should create transport with command and args", () => {
      transport = new StdioTransport(command, args);
      expect(transport).toBeDefined();
    });

    it("should create transport with command, args, and env", () => {
      transport = new StdioTransport(command, args, env);
      expect(transport).toBeDefined();
    });
  });

  describe("start", () => {
    it("should start the child process", async () => {
      transport = new StdioTransport(command, args, env);
      await transport.start();
      expect(transport.isRunning()).toBe(true);
    });

    it("should not start if already running", async () => {
      transport = new StdioTransport(command);
      await transport.start();
      const firstStart = transport.isRunning();
      await transport.start();
      const secondStart = transport.isRunning();

      expect(firstStart).toBe(true);
      expect(secondStart).toBe(true);
    });
  });

  describe("send", () => {
    it("should send request and receive response", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
        params: { foo: "bar" },
      };

      const expectedResponse: McpResponse = {
        jsonrpc: "2.0",
        id: 1,
        result: { success: true },
      };

      // Simulate response after a short delay
      const sendPromise = transport.send(request);
      setTimeout(() => {
        mockProcess.stdout.emit("data", Buffer.from(JSON.stringify(expectedResponse) + "\n"));
      }, 10);

      const response = await sendPromise;
      expect(response).toEqual(expectedResponse);
    });

    it("should throw error if not running", async () => {
      transport = new StdioTransport(command);
      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
      };

      await expect(transport.send(request)).rejects.toThrow("Process not running");
    });

    it("should handle error responses", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
      };

      const errorResponse: McpResponse = {
        jsonrpc: "2.0",
        id: 1,
        error: {
          code: -32601,
          message: "Method not found",
        },
      };

      const sendPromise = transport.send(request);
      setTimeout(() => {
        mockProcess.stdout.emit("data", Buffer.from(JSON.stringify(errorResponse) + "\n"));
      }, 10);

      const response = await sendPromise;
      expect(response.error).toBeDefined();
      expect(response.error?.code).toBe(-32601);
    });

    it("should handle multiple pending requests", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request1: McpRequest = { jsonrpc: "2.0", id: 1, method: "test1" };
      const request2: McpRequest = { jsonrpc: "2.0", id: 2, method: "test2" };

      const response1Promise = transport.send(request1);
      const response2Promise = transport.send(request2);

      setTimeout(() => {
        mockProcess.stdout.emit(
          "data",
          Buffer.from(JSON.stringify({ jsonrpc: "2.0", id: 2, result: "result2" }) + "\n")
        );
        mockProcess.stdout.emit(
          "data",
          Buffer.from(JSON.stringify({ jsonrpc: "2.0", id: 1, result: "result1" }) + "\n")
        );
      }, 10);

      const [response1, response2] = await Promise.all([response1Promise, response2Promise]);
      expect(response1.id).toBe(1);
      expect(response2.id).toBe(2);
    });

    it("should handle partial JSON messages", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
      };

      const fullResponse = JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        result: { success: true },
      });

      const sendPromise = transport.send(request);
      setTimeout(() => {
        // Send response in chunks
        mockProcess.stdout.emit("data", Buffer.from(fullResponse.substring(0, 20)));
        mockProcess.stdout.emit("data", Buffer.from(fullResponse.substring(20) + "\n"));
      }, 10);

      const response = await sendPromise;
      expect(response.id).toBe(1);
    });
  });

  describe("stop", () => {
    it("should stop the running process", async () => {
      transport = new StdioTransport(command);
      await transport.start();
      expect(transport.isRunning()).toBe(true);

      await transport.stop();
      expect(transport.isRunning()).toBe(false);
    });

    it("should not throw error if already stopped", async () => {
      transport = new StdioTransport(command);
      await expect(transport.stop()).resolves.not.toThrow();
    });

    it("should reject pending requests on stop", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
      };

      const sendPromise = transport.send(request).catch((error) => error);

      // Stop immediately
      await transport.stop();

      const result = await sendPromise;
      expect(result).toBeInstanceOf(Error);
    });
  });

  describe("isRunning", () => {
    it("should return false when not started", () => {
      transport = new StdioTransport(command);
      expect(transport.isRunning()).toBe(false);
    });

    it("should return true when started", async () => {
      transport = new StdioTransport(command);
      await transport.start();
      expect(transport.isRunning()).toBe(true);
    });

    it("should return false after stopped", async () => {
      transport = new StdioTransport(command);
      await transport.start();
      await transport.stop();
      expect(transport.isRunning()).toBe(false);
    });
  });

  describe("error handling", () => {
    it("should handle process exit", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const mockProcess = (transport as any).process;
      mockProcess.emit("exit", 0);

      expect(transport.isRunning()).toBe(false);
    });

    it("should handle process errors", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const mockProcess = (transport as any).process;
      mockProcess.emit("error", new Error("Process error"));

      expect(transport.isRunning()).toBe(false);
    });

    it("should reject pending requests on process error", async () => {
      transport = new StdioTransport(command);
      await transport.start();

      const request: McpRequest = {
        jsonrpc: "2.0",
        id: 1,
        method: "test",
      };

      const sendPromise = transport.send(request);

      setTimeout(() => {
        mockProcess.emit("error", new Error("Process crashed"));
      }, 10);

      await expect(sendPromise).rejects.toThrow();
    });
  });
});
