import { describe, test, expect, beforeAll, afterEach } from "bun:test";
import { ServerManager, ServerState } from "../src/server/ServerManager";
import { OpenCodeSettings } from "../src/types";

// Test configuration
const TEST_PORT_BASE = 15000;
const TEST_TIMEOUT_MS = 10000; // 10 seconds for server startup in tests
const PROJECT_DIR = process.cwd();

let currentPort = TEST_PORT_BASE;

function getNextPort(): number {
  return currentPort++;
}

function createTestSettings(port: number): OpenCodeSettings {
  return {
    port,
    hostname: "127.0.0.1",
    autoStart: false,
    opencodePath: "opencode",
    projectDirectory: "",
    startupTimeout: process.platform === "win32" ? 15000 : TEST_TIMEOUT_MS,
    defaultViewLocation: "sidebar",
    injectWorkspaceContext: true,
    maxNotesInContext: 20,
    maxSelectionLength: 2000,
    customCommand: "",
    useCustomCommand: false,
  };
}

// Track current manager for cleanup
let currentManager: ServerManager | null = null;

// Verify opencode binary is available before running tests
beforeAll(async () => {
  const proc = Bun.spawn(["opencode", "--version"], {
    stdout: "pipe",
    stderr: "pipe",
  });
  const exitCode = await proc.exited;

  if (exitCode !== 0) {
    throw new Error(
      "opencode binary not found or not executable. " +
        "Please ensure 'opencode' is installed and available in PATH."
    );
  }
});

// Cleanup after each test
afterEach(async () => {
  if (currentManager) {
    await currentManager.stop();
    // Give process time to fully terminate
    await new Promise((resolve) => setTimeout(resolve, 500));
    currentManager = null;
  }
});

describe("ServerManager", () => {
  describe("happy path", () => {
     test("starts server and transitions to running state", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);
       const stateHistory: ServerState[] = [];

       currentManager = new ServerManager(settings, PROJECT_DIR);
       currentManager.on("stateChange", (state: ServerState) => {
         stateHistory.push(state);
       });

      expect(currentManager.getState()).toBe("stopped");

      const success = await currentManager.start();

      expect(success).toBe(true);
      expect(currentManager.getState()).toBe("running");
      expect(stateHistory).toContain("starting");
      expect(stateHistory).toContain("running");
    }, 30000); // Increased timeout for database migration on first run

     test("reports correct server URL with encoded project directory", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);

       currentManager = new ServerManager(settings, PROJECT_DIR);

      const url = currentManager.getUrl();
      const expectedBase = `http://127.0.0.1:${port}`;
      const expectedPath = Buffer.from(PROJECT_DIR).toString('base64');

      expect(url).toBe(`${expectedBase}/${expectedPath}`);
    });

     test("stops server gracefully and transitions to stopped state", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);
       const stateHistory: ServerState[] = [];

       currentManager = new ServerManager(settings, PROJECT_DIR);
       currentManager.on("stateChange", (state: ServerState) => {
         stateHistory.push(state);
       });

      await currentManager.start();
      expect(currentManager.getState()).toBe("running");

      await currentManager.stop();

      expect(currentManager.getState()).toBe("stopped");
      expect(stateHistory).toContain("stopped");
    });

     test("state callbacks fire in correct order: starting -> running", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);
       const stateHistory: ServerState[] = [];

       currentManager = new ServerManager(settings, PROJECT_DIR);
       currentManager.on("stateChange", (state: ServerState) => {
         stateHistory.push(state);
       });

      await currentManager.start();

      // Verify order: first starting, then running
      const startingIndex = stateHistory.indexOf("starting");
      const runningIndex = stateHistory.indexOf("running");

      expect(startingIndex).toBeGreaterThanOrEqual(0);
      expect(runningIndex).toBeGreaterThan(startingIndex);
    });

     test("can restart after stop", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);

       currentManager = new ServerManager(settings, PROJECT_DIR);

      // First start
      const firstStart = await currentManager.start();
      expect(firstStart).toBe(true);
      expect(currentManager.getState()).toBe("running");

      // Stop
      await currentManager.stop();
      expect(currentManager.getState()).toBe("stopped");

      // Wait for process to fully terminate
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // Restart
      const secondStart = await currentManager.start();
      expect(secondStart).toBe(true);
      expect(currentManager.getState()).toBe("running");
    });

     test("returns true immediately if already running", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);

       currentManager = new ServerManager(settings, PROJECT_DIR);

      // First start
      await currentManager.start();
      expect(currentManager.getState()).toBe("running");

      // Second start should return true immediately without state changes
      const stateHistory: ServerState[] = [];
      const onStateChange = (state: ServerState) => {
        stateHistory.push(state);
      };
      currentManager.on("stateChange", onStateChange);

      const result = await currentManager.start();

      expect(result).toBe(true);
      expect(currentManager.getState()).toBe("running");
      // Should not have triggered any state changes
      expect(stateHistory).toEqual([]);
    });

     test("health check endpoint is accessible when running", async () => {
       const port = getNextPort();
       const settings = createTestSettings(port);

       currentManager = new ServerManager(settings, PROJECT_DIR);

      await currentManager.start();

      // Verify we can hit the health endpoint
      const url = currentManager.getUrl();
      const healthUrl = `${url}/global/health`;

      const response = await fetch(healthUrl, {
        signal: AbortSignal.timeout(2000),
      });

      expect(response.ok).toBe(true);
    });
  });

  describe("async stop behavior", () => {
    test("stop returns immediately when no process", async () => {
      const port = getNextPort();
      const settings = createTestSettings(port);
      const stateHistory: ServerState[] = [];

      currentManager = new ServerManager(settings, PROJECT_DIR);
      currentManager.on("stateChange", (state: ServerState) => {
        stateHistory.push(state);
      });

      // Stop without starting - should not throw and set state
      await currentManager.stop();

      expect(currentManager.getState()).toBe("stopped");
    });

    test("stop completes within timeout when process exits quickly", async () => {
      const port = getNextPort();
      const settings = createTestSettings(port);

      currentManager = new ServerManager(settings, PROJECT_DIR);

      await currentManager.start();
      expect(currentManager.getState()).toBe("running");

      // Stop should complete within 5 seconds (2s SIGTERM wait + 3s SIGKILL wait)
      const stopStart = Date.now();
      await currentManager.stop();
      const stopDuration = Date.now() - stopStart;

      expect(currentManager.getState()).toBe("stopped");
      // Should complete well before 5 second timeout
      expect(stopDuration).toBeLessThan(6000);
    });

    test("process is fully terminated after stop completes", async () => {
      const port = getNextPort();
      const settings = createTestSettings(port);

      currentManager = new ServerManager(settings, PROJECT_DIR);

      await currentManager.start();

      const url = currentManager.getUrl();
      
      await currentManager.stop();

      // Wait a bit then verify server is not accessible
      await new Promise((resolve) => setTimeout(resolve, 1000));

      try {
        const response = await fetch(`${url}/global/health`, {
          signal: AbortSignal.timeout(1000),
        });
        // If we get here, server is still running - test should fail
        expect(response.ok).toBe(false);
      } catch (e) {
        // Expected - server should not be accessible
        expect(e).toBeDefined();
      }
    });
  });

  describe("error handling", () => {
    test("handles double stop gracefully", async () => {
      const port = getNextPort();
      const settings = createTestSettings(port);

      currentManager = new ServerManager(settings, PROJECT_DIR);

      await currentManager.start();
      expect(currentManager.getState()).toBe("running");

      // First stop
      await currentManager.stop();
      expect(currentManager.getState()).toBe("stopped");

      // Second stop should not throw
      await currentManager.stop();
      expect(currentManager.getState()).toBe("stopped");
    });
  });

  describe("Unicode path support", () => {
    test("getUrl handles Chinese characters in project directory", () => {
      const settings = createTestSettings(getNextPort());
      const chinesePath = "C:/用户/Notes";
      const manager = new ServerManager(settings, chinesePath);

      const url = manager.getUrl();

      expect(url).toContain("http://127.0.0.1:");
      expect(url).toContain(Buffer.from(chinesePath).toString('base64'));
    });

    test("getUrl handles Japanese characters in project directory", () => {
      const settings = createTestSettings(getNextPort());
      const japanesePath = "/home/ユーザー/ノート";
      const manager = new ServerManager(settings, japanesePath);

      const url = manager.getUrl();

      expect(url).toContain(Buffer.from(japanesePath).toString('base64'));
    });

    test("getUrl handles emoji in project directory", () => {
      const settings = createTestSettings(getNextPort());
      const emojiPath = "/home/user/📁Notes";
      const manager = new ServerManager(settings, emojiPath);

      const url = manager.getUrl();

      expect(url).toContain(Buffer.from(emojiPath).toString('base64'));
    });
  });
});
