/**
 * Tests for JWTAuthImplementation
 *
 * Tests the complete JWT authentication implementation including
 * token refresh, validation, and error handling.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { JWTAuthImplementation } from "@/client/managers/JWTAuthImplementation.js";
import { AuthenticationError as _AuthenticationError } from "@/types/client.js";

// Mock RequestManager
const mockRequestManager = {
  request: vi.fn(),
};

// Mock logger
vi.mock("../../dist/utils/logger.js", () => ({
  LoggerFactory: {
    api: () => ({
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
      debug: vi.fn(),
    }),
  },
}));

describe("JWTAuthImplementation", () => {
  let jwtAuth;
  let mockAuthConfig;

  beforeEach(() => {
    mockAuthConfig = {
      method: "jwt",
      username: "testuser",
      password: "testpass",
    };

    jwtAuth = new JWTAuthImplementation(mockRequestManager, mockAuthConfig);
    vi.clearAllMocks();
  });

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

  describe("Constructor", () => {
    it("should initialize with config", () => {
      expect(jwtAuth).toBeDefined();
      expect(jwtAuth.getToken()).toBeNull();
      expect(jwtAuth.hasValidToken()).toBe(false);
    });
  });

  describe("JWT Authentication", () => {
    it("should authenticate successfully with valid credentials", async () => {
      const mockTokenResponse = {
        token: "jwt-token-123",
        user_email: "test@example.com",
        user_nicename: "testuser",
        user_display_name: "Test User",
        expires_in: 3600,
      };

      const mockValidateResponse = {
        code: "jwt_auth_valid_token",
        data: { status: 200 },
      };

      mockRequestManager.request
        .mockResolvedValueOnce(mockTokenResponse) // Auth request
        .mockResolvedValueOnce(mockValidateResponse); // Validation request

      await jwtAuth.authenticateJWT();

      expect(mockRequestManager.request).toHaveBeenCalledWith(
        "POST",
        "jwt-auth/v1/token",
        {
          username: "testuser",
          password: "testpass",
        },
        expect.objectContaining({
          skipAuth: true,
          timeout: 10000,
        }),
      );

      expect(jwtAuth.getToken()).toBe("jwt-token-123");
      expect(jwtAuth.hasValidToken()).toBe(true);
    });

    it("should throw error for missing credentials", async () => {
      const invalidConfig = { method: "jwt" };
      const invalidJWT = new JWTAuthImplementation(mockRequestManager, invalidConfig);

      await expect(invalidJWT.authenticateJWT()).rejects.toThrow("JWT authentication requires username and password");
    });

    it("should throw error when no token received", async () => {
      mockRequestManager.request.mockResolvedValueOnce({
        user_email: "test@example.com",
        // Missing token
      });

      await expect(jwtAuth.authenticateJWT()).rejects.toThrow("JWT token not received in response");
    });

    it("should handle authentication errors", async () => {
      mockRequestManager.request.mockRejectedValueOnce(new Error("Network error"));

      await expect(jwtAuth.authenticateJWT()).rejects.toThrow("JWT authentication failed: Network error");

      expect(jwtAuth.getToken()).toBeNull();
      expect(jwtAuth.hasValidToken()).toBe(false);
    });
  });

  describe("Token Validation", () => {
    beforeEach(() => {
      // Set up authenticated state
      jwtAuth.jwtToken = "test-token";
      jwtAuth.tokenExpiry = Date.now() + 3600000; // 1 hour from now
    });

    it("should validate token successfully", async () => {
      mockRequestManager.request.mockResolvedValueOnce({
        code: "jwt_auth_valid_token",
        data: { status: 200 },
      });

      const isValid = await jwtAuth.validateToken();

      expect(isValid).toBe(true);
      expect(mockRequestManager.request).toHaveBeenCalledWith(
        "POST",
        "jwt-auth/v1/token/validate",
        {},
        expect.objectContaining({
          headers: {
            Authorization: "Bearer test-token",
          },
          timeout: 5000,
        }),
      );
    });

    it("should handle invalid token", async () => {
      mockRequestManager.request.mockResolvedValueOnce({
        code: "jwt_auth_invalid_token",
        data: { status: 401 },
      });

      const isValid = await jwtAuth.validateToken();

      expect(isValid).toBe(false);
      expect(jwtAuth.getToken()).toBeNull();
    });

    it("should handle validation errors", async () => {
      mockRequestManager.request.mockRejectedValueOnce(new Error("Validation error"));

      const isValid = await jwtAuth.validateToken();

      expect(isValid).toBe(false);
    });

    it("should return false when no token exists", async () => {
      jwtAuth.clearToken();

      const isValid = await jwtAuth.validateToken();

      expect(isValid).toBe(false);
      expect(mockRequestManager.request).not.toHaveBeenCalled();
    });
  });

  describe("Token Refresh", () => {
    beforeEach(() => {
      // Set up with expired token
      jwtAuth.jwtToken = "expired-token";
      jwtAuth.tokenExpiry = Date.now() - 1000; // Expired 1 second ago
    });

    it("should refresh token using refresh endpoint", async () => {
      mockRequestManager.request.mockResolvedValueOnce({
        token: "refreshed-token-456",
        expires_in: 3600,
      });

      await jwtAuth.refreshToken();

      expect(mockRequestManager.request).toHaveBeenCalledWith(
        "POST",
        "jwt-auth/v1/token/refresh",
        {},
        expect.objectContaining({
          headers: {
            Authorization: "Bearer expired-token",
          },
          timeout: 5000,
        }),
      );

      expect(jwtAuth.getToken()).toBe("refreshed-token-456");
      expect(jwtAuth.hasValidToken()).toBe(true);
    });

    it("should fall back to re-authentication when refresh fails", async () => {
      // Mock refresh endpoint failure
      mockRequestManager.request
        .mockRejectedValueOnce(new Error("Refresh not supported"))
        // Then successful re-auth
        .mockResolvedValueOnce({
          token: "new-auth-token",
          user_email: "test@example.com",
          user_nicename: "testuser",
          user_display_name: "Test User",
          expires_in: 3600,
        })
        // And validation
        .mockResolvedValueOnce({
          code: "jwt_auth_valid_token",
          data: { status: 200 },
        });

      await jwtAuth.refreshToken();

      expect(jwtAuth.getToken()).toBe("new-auth-token");
      expect(jwtAuth.hasValidToken()).toBe(true);
    });

    it("should throw error when no token to refresh", async () => {
      jwtAuth.clearToken();

      await expect(jwtAuth.refreshToken()).rejects.toThrow("No JWT token to refresh");
    });

    it("should throw error when refresh fails and no original credentials", async () => {
      const noCredsJWT = new JWTAuthImplementation(mockRequestManager, { method: "jwt" });
      noCredsJWT.jwtToken = "test-token";

      mockRequestManager.request.mockRejectedValueOnce(new Error("Refresh failed"));

      await expect(noCredsJWT.refreshToken()).rejects.toThrow(
        "Cannot refresh JWT token: original credentials not available",
      );
    });
  });

  describe("Token Expiry", () => {
    it("should detect expired tokens", () => {
      jwtAuth.tokenExpiry = Date.now() - 1000; // Expired

      expect(jwtAuth.isTokenExpired()).toBe(true);
    });

    it("should detect tokens expiring within 5 minutes", () => {
      jwtAuth.tokenExpiry = Date.now() + 4 * 60 * 1000; // 4 minutes from now

      expect(jwtAuth.isTokenExpired()).toBe(true); // Should be considered expired
    });

    it("should detect valid tokens", () => {
      jwtAuth.tokenExpiry = Date.now() + 10 * 60 * 1000; // 10 minutes from now

      expect(jwtAuth.isTokenExpired()).toBe(false);
    });

    it("should return true when no expiry set", () => {
      jwtAuth.tokenExpiry = null;

      expect(jwtAuth.isTokenExpired()).toBe(true);
    });
  });

  describe("Automatic Token Management", () => {
    it("should authenticate when no token exists", async () => {
      const mockTokenResponse = {
        token: "new-token",
        user_email: "test@example.com",
        user_nicename: "testuser",
        user_display_name: "Test User",
        expires_in: 3600,
      };

      mockRequestManager.request
        .mockResolvedValueOnce(mockTokenResponse)
        .mockResolvedValueOnce({ code: "jwt_auth_valid_token", data: { status: 200 } });

      await jwtAuth.ensureValidToken();

      expect(jwtAuth.getToken()).toBe("new-token");
      expect(jwtAuth.hasValidToken()).toBe(true);
    });

    it("should refresh when token is expired", async () => {
      jwtAuth.jwtToken = "expired-token";
      jwtAuth.tokenExpiry = Date.now() - 1000;

      mockRequestManager.request.mockResolvedValueOnce({
        token: "refreshed-token",
        expires_in: 3600,
      });

      await jwtAuth.ensureValidToken();

      expect(jwtAuth.getToken()).toBe("refreshed-token");
    });

    it("should do nothing when token is still valid", async () => {
      jwtAuth.jwtToken = "valid-token";
      jwtAuth.tokenExpiry = Date.now() + 10 * 60 * 1000;

      await jwtAuth.ensureValidToken();

      expect(mockRequestManager.request).not.toHaveBeenCalled();
      expect(jwtAuth.getToken()).toBe("valid-token");
    });
  });

  describe("Auth Headers", () => {
    it("should return empty headers when no token", () => {
      const headers = jwtAuth.getAuthHeaders();

      expect(headers).toEqual({});
    });

    it("should return Bearer token header when token exists", () => {
      jwtAuth.jwtToken = "test-token";

      const headers = jwtAuth.getAuthHeaders();

      expect(headers).toEqual({
        Authorization: "Bearer test-token",
      });
    });
  });

  describe("Logout", () => {
    beforeEach(() => {
      jwtAuth.jwtToken = "test-token";
      jwtAuth.tokenExpiry = Date.now() + 3600000;
    });

    it("should invalidate token on server and clear local token", async () => {
      mockRequestManager.request.mockResolvedValueOnce({ success: true });

      await jwtAuth.logout();

      expect(mockRequestManager.request).toHaveBeenCalledWith(
        "POST",
        "jwt-auth/v1/token/invalidate",
        {},
        expect.objectContaining({
          headers: {
            Authorization: "Bearer test-token",
          },
          timeout: 5000,
        }),
      );

      expect(jwtAuth.getToken()).toBeNull();
      expect(jwtAuth.hasValidToken()).toBe(false);
    });

    it("should clear local token even if server invalidation fails", async () => {
      mockRequestManager.request.mockRejectedValueOnce(new Error("Server error"));

      await jwtAuth.logout();

      expect(jwtAuth.getToken()).toBeNull();
      expect(jwtAuth.hasValidToken()).toBe(false);
    });

    it("should handle logout when no token exists", async () => {
      jwtAuth.clearToken();

      await jwtAuth.logout();

      expect(mockRequestManager.request).not.toHaveBeenCalled();
    });
  });

  describe("Token Info", () => {
    it("should return token info when token exists", () => {
      const expiryTime = Date.now() + 30 * 60 * 1000; // 30 minutes
      jwtAuth.jwtToken = "test-token";
      jwtAuth.tokenExpiry = expiryTime;

      const info = jwtAuth.getTokenInfo();

      expect(info).toEqual({
        hasToken: true,
        isExpired: false,
        expiresAt: new Date(expiryTime).toISOString(),
        expiresIn: expect.any(Number),
      });

      expect(info.expiresIn).toBeGreaterThan(25 * 60 * 1000); // Should be close to 30 minutes
    });

    it("should return token info when no token exists", () => {
      const info = jwtAuth.getTokenInfo();

      expect(info).toEqual({
        hasToken: false,
        isExpired: true,
        expiresAt: null,
        expiresIn: null,
      });
    });
  });

  describe("Error Handling", () => {
    it("should handle network errors during authentication", async () => {
      mockRequestManager.request.mockRejectedValueOnce(new Error("ECONNREFUSED"));

      await expect(jwtAuth.authenticateJWT()).rejects.toThrow("JWT authentication failed: ECONNREFUSED");
    });

    it("should handle malformed responses", async () => {
      mockRequestManager.request.mockResolvedValueOnce({
        invalid: "response",
        // Missing required token field
      });

      await expect(jwtAuth.authenticateJWT()).rejects.toThrow("JWT token not received in response");
    });

    it("should handle server errors during refresh", async () => {
      jwtAuth.jwtToken = "test-token";
      mockRequestManager.request.mockRejectedValueOnce({
        status: 500,
        message: "Internal server error",
      });

      await expect(jwtAuth.refreshToken()).rejects.toThrow("JWT token refresh failed");
    });
  });
});
