import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import axios from "axios";
import { BROKER_EVENTS } from "../broker/broker-events";

vi.mock("axios");

const access_token = "test_access_token";
const refresh_token = "test_refresh_token";
const new_access_token = "new_access_token";
const new_refresh_token = "new_refresh_token";

const createMockBroker = () => ({
  publish: vi.fn().mockResolvedValue(undefined),
  subscribe: vi.fn(),
  send: vi.fn(),
  registerRequest: vi.fn(),
  events: BROKER_EVENTS,
});

describe("TokenManager", () => {
  let mockLocation: Partial<Location>;
  let TokenManagerImpl: any;
  let createTokenManager: any;
  let mockBroker: ReturnType<typeof createMockBroker>;

  beforeEach(async () => {
    vi.resetModules();
    mockLocation = {
      search: `?access_token=${access_token}&refresh_token=${refresh_token}`,
    };
    vi.spyOn(window, "location", "get").mockReturnValue(mockLocation as Location);
    mockBroker = createMockBroker();

    const module = await import("./token-manager");
    TokenManagerImpl = module.TokenManagerImpl;
    createTokenManager = module.createTokenManager;
  });

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

  describe("getToken", () => {
    it("should initialize token from URL params on first call", () => {
      const tokenManager = new TokenManagerImpl(mockBroker);
      expect(tokenManager.getToken()).toBe(access_token);
    });

    it("should return empty string if URL params are missing", () => {
      mockLocation.search = "";
      const tokenManager = new TokenManagerImpl(mockBroker);
      expect(tokenManager.getToken()).toBe("");
    });

    it("should return the same token on subsequent calls", () => {
      const tokenManager = new TokenManagerImpl(mockBroker);
      expect(tokenManager.getToken()).toBe(tokenManager.getToken());
    });
  });

  describe("setInitialTokens", () => {
    it("should set tokens manually", () => {
      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens("manual_access", "manual_refresh");
      expect(tokenManager.getToken()).toBe("manual_access");
    });

    it("should throw error if tokens are already initialized", () => {
      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens("manual_access", "manual_refresh");
      expect(() => tokenManager.setInitialTokens("another_access", "another_refresh")).toThrow(
        "Token already initialized",
      );
    });

    it("should throw error if tokens were already initialized from URL", () => {
      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.getToken();
      expect(() => tokenManager.setInitialTokens("manual_access", "manual_refresh")).toThrow(
        "Token already initialized",
      );
    });
  });

  describe("refreshToken", () => {
    it("should refresh token successfully", async () => {
      vi.mocked(axios.post).mockResolvedValue({
        data: { access_token: new_access_token, refresh_token: new_refresh_token },
      });

      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens(access_token, refresh_token);

      const refreshed = await tokenManager.refreshToken();

      expect(axios.post).toHaveBeenCalledWith("/api/token/refresh", { token: refresh_token });
      expect(refreshed).toBe(new_access_token);
      expect(tokenManager.getToken()).toBe(new_access_token);
    });

    it("should initialize tokens from URL before refreshing if not initialized", async () => {
      vi.mocked(axios.post).mockResolvedValue({
        data: { access_token: new_access_token, refresh_token: new_refresh_token },
      });

      const tokenManager = new TokenManagerImpl(mockBroker);
      await tokenManager.refreshToken();

      expect(axios.post).toHaveBeenCalledWith("/api/token/refresh", { token: refresh_token });
    });

    it("should throw error if refresh response doesn't contain access_token", async () => {
      vi.mocked(axios.post).mockResolvedValue({ data: {} });

      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens(access_token, refresh_token);

      await expect(tokenManager.refreshToken()).rejects.toThrow("Invalid refresh token response");
    });

    it("should publish refreshTokenFailed and rethrow when refresh fails", async () => {
      vi.mocked(axios.post).mockRejectedValue(new Error("Network error"));

      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens(access_token, refresh_token);

      await expect(tokenManager.refreshToken()).rejects.toThrow("Network error");
      expect(mockBroker.publish).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {});
    });

    it("should not publish refreshTokenFailed when refresh succeeds", async () => {
      vi.mocked(axios.post).mockResolvedValue({
        data: { access_token: new_access_token, refresh_token: new_refresh_token },
      });

      const tokenManager = new TokenManagerImpl(mockBroker);
      tokenManager.setInitialTokens(access_token, refresh_token);

      await tokenManager.refreshToken();
      expect(mockBroker.publish).not.toHaveBeenCalled();
    });
  });

  describe("createTokenManager", () => {
    it("should return a singleton instance", () => {
      expect(createTokenManager(mockBroker)).toBe(createTokenManager(mockBroker));
    });
  });
});
