import { noOpLogger } from "@mcpx/toolkit-core/logging";
import { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
import {
  McpxOAuthProviderI,
  OAuthProviderType,
} from "../oauth-providers/model.js";
import { OAuthSessionManagerI } from "../server/oauth-session-manager.js";
import { ExtendedClientBuilderI, ExtendedClientI } from "./client-extension.js";
import {
  isAuthenticationError,
  OAuthConnectionHandler,
} from "./oauth-connection-handler.js";

describe("OAuthConnectionHandler", () => {
  describe(".isAuthenticationError", () => {
    it("should return true for 401 status in response", () => {
      const error = { response: { status: 401 } };
      expect(isAuthenticationError(error)).toBe(true);
    });

    it("should return true for direct 401 status", () => {
      const error = { status: 401 };
      expect(isAuthenticationError(error)).toBe(true);
    });

    it("should return true for 401 code", () => {
      const error = { code: 401 };
      expect(isAuthenticationError(error)).toBe(true);
    });

    it("should return true for message containing '401'", () => {
      const error = { message: "HTTP 401 Unauthorized" };
      expect(isAuthenticationError(error)).toBe(true);
    });

    it("should return true for message containing 'Unauthorized'", () => {
      const error = { message: "Unauthorized access" };
      expect(isAuthenticationError(error)).toBe(true);
    });

    it("should return false for non-401 errors", () => {
      const error = { status: 500 };
      expect(isAuthenticationError(error)).toBe(false);
    });

    it("should return false for non-object errors", () => {
      expect(isAuthenticationError("string error")).toBe(false);
      expect(isAuthenticationError(null)).toBe(false);
      expect(isAuthenticationError(undefined)).toBe(false);
    });

    it("should return false for objects without auth-related properties", () => {
      const error = { message: "Some other error", code: 500 };
      expect(isAuthenticationError(error)).toBe(false);
    });
  });

  describe("two-phase OAuth flow", () => {
    const TEST_SERVER_NAME = "test-server";
    const TEST_STATE = "test-state-123";
    const TEST_AUTH_URL = new URL(
      "https://auth.example.com/authorize?state=test",
    );
    const TEST_AUTH_CODE = "test-auth-code-456";

    function createMockProvider(
      overrides: Partial<McpxOAuthProviderI> = {},
    ): McpxOAuthProviderI {
      return {
        type: "dcr" as OAuthProviderType,
        serverName: TEST_SERVER_NAME,
        state: () => TEST_STATE,
        completeAuthorization: () => {},
        getAuthorizationCode: () => null,
        getAuthorizationUrl: () => TEST_AUTH_URL,
        getUserCode: () => null,
        redirectUrl: "http://localhost:9000/oauth/callback",
        clientMetadata: {
          redirect_uris: ["http://localhost:9000/oauth/callback"],
        },
        clientInformation: async () => undefined,
        tokens: async () => undefined,
        saveTokens: async (_tokens: OAuthTokens) => {},
        redirectToAuthorization: async (_url: URL) => {},
        saveCodeVerifier: async (_verifier: string) => {},
        codeVerifier: async () => "",
        ...overrides,
      } as McpxOAuthProviderI;
    }

    function createMockSessionManager(
      provider: McpxOAuthProviderI,
    ): OAuthSessionManagerI {
      const flows = new Map<
        string,
        {
          serverName: string;
          serverUrl: string;
          state: string;
          createdAt: Date;
        }
      >();

      return {
        getOrCreateOAuthProvider: () => provider,
        startOAuthFlow: (serverName, serverUrl, state) => {
          flows.set(state, {
            serverName,
            serverUrl,
            state,
            createdAt: new Date(),
          });
        },
        getOAuthFlow: (state) => flows.get(state),
        completeOAuthFlow: (state) => {
          const flow = flows.get(state);
          flows.delete(state);
          return flow;
        },
        deleteOAuthTokensForServer: async (_serverName) => {},
        hasOAuthProvider: (_serverName) => false,
        getExistingOAuthProvider: (_serverName) => undefined,
      };
    }

    function createMockExtendedClientBuilder(): ExtendedClientBuilderI {
      return {
        build: async () =>
          ({
            close: async () => {},
            listTools: async () => ({ tools: [], toolParentNames: {} }),
            callTool: async () => ({ content: [] }),
          }) as unknown as ExtendedClientI,
      };
    }

    describe(".cancelPendingOAuth", () => {
      it("should return false when no pending flow to cancel", () => {
        const provider = createMockProvider();
        const sessionManager = createMockSessionManager(provider);
        const clientBuilder = createMockExtendedClientBuilder();

        const handler = new OAuthConnectionHandler(
          sessionManager,
          clientBuilder,
          noOpLogger,
        );

        expect(handler.cancelPendingOAuth(TEST_SERVER_NAME)).toBe(false);
      });
    });

    describe(".getServerNameByState", () => {
      it("should return null when no flow exists for state", () => {
        const provider = createMockProvider();
        const sessionManager = createMockSessionManager(provider);
        const clientBuilder = createMockExtendedClientBuilder();

        const handler = new OAuthConnectionHandler(
          sessionManager,
          clientBuilder,
          noOpLogger,
        );

        expect(handler.getServerNameByState("unknown-state")).toBeNull();
      });
    });

    describe(".completeOAuth", () => {
      it("should throw when no pending flow exists", async () => {
        const provider = createMockProvider();
        const sessionManager = createMockSessionManager(provider);
        const clientBuilder = createMockExtendedClientBuilder();

        const handler = new OAuthConnectionHandler(
          sessionManager,
          clientBuilder,
          noOpLogger,
        );

        await expect(
          handler.completeOAuth(TEST_SERVER_NAME, TEST_AUTH_CODE),
        ).rejects.toThrow(
          `No pending OAuth flow for server: ${TEST_SERVER_NAME}`,
        );
      });
    });

    describe(".deleteOAuthTokensForServer", () => {
      it("should call session manager deleteOAuthTokensForServer", async () => {
        const provider = createMockProvider();
        let deletedServer: string | undefined;
        const sessionManager: OAuthSessionManagerI = {
          ...createMockSessionManager(provider),
          deleteOAuthTokensForServer: async (serverName: string) => {
            deletedServer = serverName;
          },
        };
        const handler = new OAuthConnectionHandler(
          sessionManager,
          createMockExtendedClientBuilder(),
          noOpLogger,
        );

        await handler.deleteOAuthTokensForServer(TEST_SERVER_NAME);

        expect(deletedServer).toBe(TEST_SERVER_NAME);
      });

      it("should not throw when there is no pending OAuth flow", async () => {
        const provider = createMockProvider();
        const sessionManager = createMockSessionManager(provider);
        const handler = new OAuthConnectionHandler(
          sessionManager,
          createMockExtendedClientBuilder(),
          noOpLogger,
        );

        await expect(
          handler.deleteOAuthTokensForServer(TEST_SERVER_NAME),
        ).resolves.not.toThrow();
      });
    });

    // Note: initiateOAuth is difficult to unit test because it creates real
    // transport instances that attempt network connections. Full integration
    // testing of the two-phase flow is covered by oauth.it.test.ts.
    // The key behaviors (storing pending flows, returning auth URL) are
    // implicitly tested through the IT tests.

    describe(".isTokenExpiredForServer", () => {
      const targetServer = {
        name: TEST_SERVER_NAME,
        type: "streamable-http" as const,
        url: "https://example.com",
      };

      it("returns false when no provider is registered for the server", async () => {
        const provider = createMockProvider();
        const sessionManager: OAuthSessionManagerI = {
          ...createMockSessionManager(provider),
          getExistingOAuthProvider: () => undefined,
        };
        const handler = new OAuthConnectionHandler(
          sessionManager,
          createMockExtendedClientBuilder(),
          noOpLogger,
        );

        expect(await handler.isTokenExpiredForServer(targetServer)).toBe(false);
      });

      it("returns false when provider has valid tokens", async () => {
        const provider = createMockProvider({
          tokens: async () => ({
            access_token: "tok",
            token_type: "bearer",
          }),
        });
        const sessionManager: OAuthSessionManagerI = {
          ...createMockSessionManager(provider),
          getExistingOAuthProvider: () => provider,
        };
        const handler = new OAuthConnectionHandler(
          sessionManager,
          createMockExtendedClientBuilder(),
          noOpLogger,
        );

        expect(await handler.isTokenExpiredForServer(targetServer)).toBe(false);
      });

      it("returns true when provider has no tokens (expired or never saved)", async () => {
        const provider = createMockProvider({
          tokens: async () => undefined,
        });
        const sessionManager: OAuthSessionManagerI = {
          ...createMockSessionManager(provider),
          getExistingOAuthProvider: () => provider,
        };
        const handler = new OAuthConnectionHandler(
          sessionManager,
          createMockExtendedClientBuilder(),
          noOpLogger,
        );

        expect(await handler.isTokenExpiredForServer(targetServer)).toBe(true);
      });
    });
  });
});
