import { describe, expect, test, spyOn } from "bun:test";
import { DaemonMcpProxy } from "../../src/daemon/daemonMcpProxy";
import { DaemonClient, DaemonUnavailableError } from "../../src/daemon/client";
import { DAEMON_VERSION, DAEMON_VERSION_RESTART_COOLDOWN_MS } from "../../src/daemon/constants";
import { FakeDaemonManager } from "../fakes/FakeDaemonManager";
import { FakeDaemonClient } from "../fakes/FakeDaemonClient";

const OLDER_VERSION = "0.0.1";
const NEWER_VERSION = "9999.0.0";
const ANCIENT_TIMESTAMP = 1;

describe("DaemonMcpProxy", () => {
  describe("connection management", () => {
    test("connects to daemon on first request", async () => {
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["tools/list", { tools: [{ name: "testTool", inputSchema: {} }] }]
        ])
      });
      const fakeManager = new FakeDaemonManager();

      // Mock DaemonClient.isAvailable to return true
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        daemonManager: fakeManager,
        autoStartDaemon: false
      });

      try {
        await proxy.listTools();

        expect(fakeClient.isConnected()).toBe(true);
        expect(fakeClient.callDaemonMethodCalls).toHaveLength(1);
        expect(fakeClient.callDaemonMethodCalls[0].method).toBe("tools/list");
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });

    test("auto-starts daemon when not running", async () => {
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["tools/list", { tools: [] }]
        ])
      });
      const fakeManager = new FakeDaemonManager();
      fakeManager.statusResult = { running: false };

      // Mock DaemonClient.isAvailable to return false initially, then true after start
      let isAvailableCalls = 0;
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockImplementation(async () => {
        isAvailableCalls++;
        return isAvailableCalls > 1;
      });

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        daemonManager: fakeManager,
        autoStartDaemon: true
      });

      try {
        await proxy.listTools();

        expect(fakeManager.startCalled).toBe(true);
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });

    describe("version-mismatch handling", () => {
      function makeProxy(opts: {
        runningVersion?: string;
        startedAt?: number;
        autoStartDaemon?: boolean;
        waitForReadyResult?: boolean;
        daemonOptions?: { debug?: boolean; port?: number };
      } = {}) {
        const fakeClient = new FakeDaemonClient({
          daemonMethodResults: new Map([["tools/list", { tools: [] }]]),
        });
        const fakeManager = new FakeDaemonManager();
        fakeManager.statusResult = {
          running: true,
          pid: 1234,
          port: 3000,
          socketPath: "/tmp/test.sock",
          ...(opts.runningVersion !== undefined ? { version: opts.runningVersion } : {}),
          ...(opts.startedAt !== undefined ? { startedAt: opts.startedAt } : {}),
        };
        if (opts.waitForReadyResult !== undefined) {
          fakeManager.waitForReadyResult = opts.waitForReadyResult;
        }
        const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);
        const proxy = new DaemonMcpProxy({
          clientFactory: () => fakeClient,
          daemonManager: fakeManager,
          autoStartDaemon: opts.autoStartDaemon ?? true,
          daemonOptions: opts.daemonOptions,
        });
        return { fakeClient, fakeManager, isAvailableSpy, proxy };
      }

      test("restarts daemon when MCP server version is newer", async () => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(true);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("does not restart when daemon version is newer (older client connects anyway)", async () => {
        const { fakeClient, fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: NEWER_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
          expect(fakeClient.isConnected()).toBe(true);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("does not restart when versions match", async () => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: DAEMON_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("does not restart when daemon is older but within cooldown window", async () => {
        const { fakeClient, fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: Date.now() - Math.floor(DAEMON_VERSION_RESTART_COOLDOWN_MS / 2),
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
          expect(fakeClient.isConnected()).toBe(true);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("restarts older daemon once cooldown has elapsed", async () => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: Date.now() - DAEMON_VERSION_RESTART_COOLDOWN_MS - 1000,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(true);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("does not restart when autoStartDaemon is disabled, even when newer", async () => {
        const { fakeClient, fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
          autoStartDaemon: false,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
          expect(fakeClient.isConnected()).toBe(true);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("throws when restarted daemon fails to become ready", async () => {
        const { fakeClient, fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
          waitForReadyResult: false,
        });
        try {
          await expect(proxy.listTools()).rejects.toThrow(DaemonUnavailableError);
          expect(fakeManager.restartCalled).toBe(true);
          expect(fakeClient.isConnected()).toBe(false);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("forwards daemonOptions when restarting", async () => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion: OLDER_VERSION,
          startedAt: ANCIENT_TIMESTAMP,
          daemonOptions: { debug: true, port: 4242 },
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(true);
          expect(fakeManager.restartOptions).toEqual({ debug: true, port: 4242 });
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test("does not restart when running daemon does not expose a version", async () => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          startedAt: ANCIENT_TIMESTAMP,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });

      test.each([
        ["prerelease tag", "0.0.21-beta.1"],
        ["unknown fallback", "unknown"],
        ["empty-after-prefix", "v"],
      ])("does not restart when version comparison is non-numeric (%s)", async (_label, runningVersion) => {
        const { fakeManager, isAvailableSpy, proxy } = makeProxy({
          runningVersion,
          startedAt: ANCIENT_TIMESTAMP,
        });
        try {
          await proxy.listTools();
          expect(fakeManager.restartCalled).toBe(false);
        } finally {
          isAvailableSpy.mockRestore();
          await proxy.close();
        }
      });
    });

    test("throws error when auto-start is disabled and daemon not running", async () => {
      const fakeClient = new FakeDaemonClient();
      const fakeManager = new FakeDaemonManager();
      fakeManager.statusResult = { running: false };

      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(false);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        daemonManager: fakeManager,
        autoStartDaemon: false
      });

      try {
        await expect(proxy.listTools()).rejects.toThrow("auto-start is disabled");
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });
  });

  describe("tool operations", () => {
    test("listTools returns tools from daemon", async () => {
      const expectedTools = [
        { name: "tapOn", description: "Tap on element", inputSchema: {} },
        { name: "observe", description: "Observe screen", inputSchema: {} }
      ];
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["tools/list", { tools: expectedTools }]
        ])
      });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        const tools = await proxy.listTools();

        expect(tools).toEqual(expectedTools);
        expect(fakeClient.callDaemonMethodCalls).toContainEqual({
          method: "tools/list",
          params: {}
        });
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });

    test("listTools caches results", async () => {
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["tools/list", { tools: [{ name: "test" }] }]
        ])
      });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        await proxy.listTools();
        await proxy.listTools();
        await proxy.listTools();

        // Should only call daemon once due to caching
        expect(fakeClient.callDaemonMethodCalls.length).toBe(1);
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });

    test("callTool forwards to daemon", async () => {
      const expectedResult = { content: [{ type: "text", text: "tapped!" }] };
      const fakeClient = new FakeDaemonClient({ toolResult: expectedResult });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        const result = await proxy.callTool("tapOn", { text: "Button" });

        expect(result).toEqual(expectedResult);
        expect(fakeClient.callToolCalls).toContainEqual({
          toolName: "tapOn",
          params: { text: "Button" }
        });
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });
  });

  describe("resource operations", () => {
    test("listResources returns resources from daemon", async () => {
      const expectedResources = [
        { uri: "automobile:devices/booted", name: "Booted devices" }
      ];
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["resources/list", { resources: expectedResources }]
        ])
      });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        const resources = await proxy.listResources();

        expect(resources).toEqual(expectedResources);
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });

    test("readResource forwards to daemon", async () => {
      const expectedResult = { contents: [{ uri: "automobile:test", text: "data" }] };
      const fakeClient = new FakeDaemonClient({ resourceResult: expectedResult });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        const result = await proxy.readResource("automobile:devices/booted");

        expect(result).toEqual(expectedResult);
        expect(fakeClient.readResourceCalls).toContain("automobile:devices/booted");
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });
  });

  describe("cache invalidation", () => {
    test("invalidateCache clears all caches", async () => {
      const fakeClient = new FakeDaemonClient({
        daemonMethodResults: new Map([
          ["tools/list", { tools: [{ name: "test" }] }],
          ["resources/list", { resources: [{ uri: "test" }] }]
        ])
      });
      const isAvailableSpy = spyOn(DaemonClient, "isAvailable").mockResolvedValue(true);

      const proxy = new DaemonMcpProxy({
        clientFactory: () => fakeClient,
        autoStartDaemon: false
      });

      try {
        // Populate caches
        await proxy.listTools();
        await proxy.listResources();

        expect(fakeClient.callDaemonMethodCalls.length).toBe(2);

        // Invalidate caches
        proxy.invalidateCache();

        // Should fetch again
        await proxy.listTools();
        await proxy.listResources();

        expect(fakeClient.callDaemonMethodCalls.length).toBe(4);
      } finally {
        isAvailableSpy.mockRestore();
        await proxy.close();
      }
    });
  });
});
