import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { UrbitSSEClient } from "./sse-client.js";

// Mock urbitFetch to avoid real network calls
vi.mock("./fetch.js", () => ({
  urbitFetch: vi.fn(),
}));

// Mock channel-ops to avoid real channel operations
vi.mock("./channel-ops.js", () => ({
  ensureUrbitChannelOpen: vi.fn().mockResolvedValue(undefined),
  pokeUrbitChannel: vi.fn().mockResolvedValue(undefined),
  scryUrbitPath: vi.fn().mockResolvedValue({}),
}));

describe("UrbitSSEClient", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe("subscribe", () => {
    it("sends subscriptions added after connect", async () => {
      const { urbitFetch } = await import("./fetch.js");
      const mockUrbitFetch = vi.mocked(urbitFetch);
      mockUrbitFetch.mockResolvedValue({
        response: { ok: true, status: 200 } as unknown as Response,
        finalUrl: "https://example.com",
        release: vi.fn().mockResolvedValue(undefined),
      });

      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
      // Simulate connected state
      (client as { isConnected: boolean }).isConnected = true;

      await client.subscribe({
        app: "chat",
        path: "/dm/~zod",
        event: () => {},
      });

      expect(mockUrbitFetch).toHaveBeenCalledTimes(1);
      const callArgs = mockUrbitFetch.mock.calls[0][0];
      expect(callArgs.path).toContain("/~/channel/");
      expect(callArgs.init?.method).toBe("PUT");

      const body = JSON.parse(callArgs.init?.body as string);
      expect(body).toHaveLength(1);
      expect(body[0]).toMatchObject({
        action: "subscribe",
        app: "chat",
        path: "/dm/~zod",
      });
    });

    it("queues subscriptions before connect", async () => {
      const { urbitFetch } = await import("./fetch.js");
      const mockUrbitFetch = vi.mocked(urbitFetch);

      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
      // Not connected yet

      await client.subscribe({
        app: "chat",
        path: "/dm/~zod",
        event: () => {},
      });

      // Should not call urbitFetch since not connected
      expect(mockUrbitFetch).not.toHaveBeenCalled();
      // But subscription should be queued
      expect(client.subscriptions).toHaveLength(1);
      expect(client.subscriptions[0]).toMatchObject({
        app: "chat",
        path: "/dm/~zod",
      });
    });
  });

  describe("updateCookie", () => {
    it("normalizes cookie when updating", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");

      // Cookie with extra parts that should be stripped
      client.updateCookie("urbauth-~zod=456; Path=/; HttpOnly");

      expect(client.cookie).toBe("urbauth-~zod=456");
    });

    it("handles simple cookie values", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");

      client.updateCookie("urbauth-~zod=newvalue");

      expect(client.cookie).toBe("urbauth-~zod=newvalue");
    });
  });

  describe("reconnection", () => {
    it("has autoReconnect enabled by default", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
      expect(client.autoReconnect).toBe(true);
    });

    it("can disable autoReconnect via options", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
        autoReconnect: false,
      });
      expect(client.autoReconnect).toBe(false);
    });

    it("stores onReconnect callback", () => {
      const onReconnect = vi.fn();
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
        onReconnect,
      });
      expect(client.onReconnect).toBe(onReconnect);
    });

    it("resets reconnect attempts on successful connect", async () => {
      const { urbitFetch } = await import("./fetch.js");
      const mockUrbitFetch = vi.mocked(urbitFetch);

      // Mock a response that returns a readable stream
      const mockStream = new ReadableStream({
        start(controller) {
          controller.close();
        },
      });

      mockUrbitFetch.mockResolvedValue({
        response: {
          ok: true,
          status: 200,
          body: mockStream,
        } as unknown as Response,
        finalUrl: "https://example.com",
        release: vi.fn().mockResolvedValue(undefined),
      });

      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
        autoReconnect: false, // Disable to prevent reconnect loop
      });
      client.reconnectAttempts = 5;

      await client.connect();

      expect(client.reconnectAttempts).toBe(0);
    });
  });

  describe("event acking", () => {
    it("tracks lastHeardEventId and ackThreshold", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");

      // Access private properties for testing
      const lastHeardEventId = (client as unknown as { lastHeardEventId: number }).lastHeardEventId;
      const ackThreshold = (client as unknown as { ackThreshold: number }).ackThreshold;

      expect(lastHeardEventId).toBe(-1);
      expect(ackThreshold).toBeGreaterThan(0);
    });
  });

  describe("constructor", () => {
    it("generates unique channel ID", () => {
      const client1 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
      const client2 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");

      expect(client1.channelId).not.toBe(client2.channelId);
    });

    it("normalizes cookie in constructor", () => {
      const client = new UrbitSSEClient(
        "https://example.com",
        "urbauth-~zod=123; Path=/; HttpOnly",
      );

      expect(client.cookie).toBe("urbauth-~zod=123");
    });

    it("sets default reconnection parameters", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");

      expect(client.maxReconnectAttempts).toBe(10);
      expect(client.reconnectDelay).toBe(1000);
      expect(client.maxReconnectDelay).toBe(30000);
    });

    it("allows overriding reconnection parameters", () => {
      const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
        maxReconnectAttempts: 5,
        reconnectDelay: 500,
        maxReconnectDelay: 10000,
      });

      expect(client.maxReconnectAttempts).toBe(5);
      expect(client.reconnectDelay).toBe(500);
      expect(client.maxReconnectDelay).toBe(10000);
    });
  });
});
