import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

/** Mock session shape returned by getSession in tests. */
interface MockSessionData {
  data: {
    user: Record<string, unknown>;
  } | null;
}

/** Words that must never appear in PDA-friendly messages. */
const FORBIDDEN_WORDS = [
  "overdue",
  "urgent",
  "must",
  "critical",
  "required",
  "error",
  "failed",
  "failure",
];

// Mock BetterAuth before importing the module under test
vi.mock("better-auth/react", () => ({
  createAuthClient: vi.fn(() => ({
    signIn: vi.fn(),
    signOut: vi.fn(),
    useSession: vi.fn(),
    getSession: vi.fn(() => Promise.resolve({ data: null })),
  })),
}));

vi.mock("better-auth/client/plugins", () => ({
  genericOAuthClient: vi.fn(() => ({})),
}));

vi.mock("./config", () => ({
  API_BASE_URL: "http://localhost:3001",
}));

// Import after mocks are set up
const { signInWithCredentials, getAccessToken, isAdmin, getSession } =
  await import("./auth-client");

/**
 * Helper to build a mock Response object that behaves like the Fetch API Response.
 */
function mockResponse(options: {
  ok: boolean;
  status: number;
  body?: Record<string, unknown>;
}): Response {
  const { ok, status, body = {} } = options;
  return {
    ok,
    status,
    json: vi.fn(() => Promise.resolve(body)),
    headers: new Headers(),
    redirected: false,
    statusText: "",
    type: "basic" as ResponseType,
    url: "",
    clone: vi.fn(),
    body: null,
    bodyUsed: false,
    arrayBuffer: vi.fn(),
    blob: vi.fn(),
    formData: vi.fn(),
    text: vi.fn(),
    bytes: vi.fn(),
  } as unknown as Response;
}

describe("signInWithCredentials", (): void => {
  const originalFetch = global.fetch;

  beforeEach((): void => {
    global.fetch = vi.fn();
  });

  afterEach((): void => {
    global.fetch = originalFetch;
    vi.restoreAllMocks();
  });

  it("should return data on successful response", async (): Promise<void> => {
    const sessionData = { user: { id: "1", name: "Alice" }, token: "abc" };
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({ ok: true, status: 200, body: sessionData })
    );

    const result = await signInWithCredentials("alice", "password123");

    expect(result).toEqual(sessionData);
    expect(global.fetch).toHaveBeenCalledWith(
      "http://localhost:3001/auth/sign-in/credentials",
      expect.objectContaining({
        method: "POST",
        credentials: "include",
        body: JSON.stringify({ email: "alice", password: "password123" }),
      })
    );
  });

  it("should throw PDA-friendly message on 401 response", async (): Promise<void> => {
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({ ok: false, status: 401, body: { message: "Unauthorized" } })
    );

    await expect(signInWithCredentials("alice", "wrong")).rejects.toThrow(
      "The email and password combination wasn't recognized."
    );
  });

  it("should throw PDA-friendly message on 401 with no body message", async (): Promise<void> => {
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({ ok: false, status: 401, body: {} })
    );

    // When there is no body message, the response object (status: 401) is used for parsing
    await expect(signInWithCredentials("alice", "wrong")).rejects.toThrow(
      "The email and password combination wasn't recognized."
    );
  });

  it("should throw PDA-friendly message on 500 response", async (): Promise<void> => {
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({
        ok: false,
        status: 500,
        body: { message: "Internal Server Error" },
      })
    );

    await expect(signInWithCredentials("alice", "pass")).rejects.toThrow(
      "The service is taking a break. Please try again in a moment."
    );
  });

  it("should throw PDA-friendly message on 500 with no body message", async (): Promise<void> => {
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({ ok: false, status: 500, body: {} })
    );

    await expect(signInWithCredentials("alice", "pass")).rejects.toThrow(
      "The service is taking a break. Please try again in a moment."
    );
  });

  it("should throw PDA-friendly message on network error (fetch throws)", async (): Promise<void> => {
    vi.mocked(global.fetch).mockRejectedValueOnce(new TypeError("Failed to fetch"));

    await expect(signInWithCredentials("alice", "pass")).rejects.toThrow(TypeError);
  });

  it("should throw PDA-friendly message on 429 rate-limited response", async (): Promise<void> => {
    vi.mocked(global.fetch).mockResolvedValueOnce(
      mockResponse({
        ok: false,
        status: 429,
        body: { message: "Too many requests" },
      })
    );

    await expect(signInWithCredentials("alice", "pass")).rejects.toThrow(
      "You've tried a few times. Take a moment and try again shortly."
    );
  });

  it("should throw PDA-friendly message when response.json() throws", async (): Promise<void> => {
    const errorSpy = vi.spyOn(console, "error").mockReturnValue(undefined);
    const resp = mockResponse({ ok: false, status: 403 });
    const jsonError = new SyntaxError("Unexpected token");
    (resp.json as ReturnType<typeof vi.fn>).mockRejectedValueOnce(jsonError);
    vi.mocked(global.fetch).mockResolvedValueOnce(resp);

    // JSON parse fails -> logs error -> falls back to response status
    await expect(signInWithCredentials("alice", "pass")).rejects.toThrow(
      "The email and password combination wasn't recognized."
    );

    expect(errorSpy).toHaveBeenCalledWith(
      "[Auth] Failed to parse error response body (HTTP 403):",
      jsonError
    );
    errorSpy.mockRestore();
  });
});

describe("signInWithCredentials PDA-friendly language compliance", (): void => {
  const originalFetch = global.fetch;

  beforeEach((): void => {
    global.fetch = vi.fn();
  });

  afterEach((): void => {
    global.fetch = originalFetch;
    vi.restoreAllMocks();
  });

  const errorScenarios: {
    name: string;
    status: number;
    body: Record<string, unknown>;
  }[] = [
    { name: "401 with message", status: 401, body: { message: "Unauthorized" } },
    { name: "401 without message", status: 401, body: {} },
    { name: "403 with message", status: 403, body: { message: "Forbidden" } },
    { name: "429 with message", status: 429, body: { message: "Too many requests" } },
    { name: "500 with message", status: 500, body: { message: "Internal Server Error" } },
    { name: "500 without message", status: 500, body: {} },
    { name: "502 without message", status: 502, body: {} },
    { name: "503 without message", status: 503, body: {} },
    { name: "400 unknown", status: 400, body: {} },
  ];

  for (const scenario of errorScenarios) {
    it(`should not contain forbidden words for ${scenario.name} response`, async (): Promise<void> => {
      vi.mocked(global.fetch).mockResolvedValueOnce(
        mockResponse({ ok: false, status: scenario.status, body: scenario.body })
      );

      try {
        await signInWithCredentials("alice", "pass");
        // Should not reach here
        expect.unreachable("signInWithCredentials should have thrown");
      } catch (thrown: unknown) {
        expect(thrown).toBeInstanceOf(Error);
        const message = (thrown as Error).message.toLowerCase();
        for (const forbidden of FORBIDDEN_WORDS) {
          expect(message).not.toContain(forbidden);
        }
      }
    });
  }
});

// ────────────────────────────────────────────────────────────────────────────
// AUTH-030: getAccessToken tests
// ────────────────────────────────────────────────────────────────────────────

describe("getAccessToken", (): void => {
  afterEach((): void => {
    vi.restoreAllMocks();
  });

  it("should return null when no session exists (session.data is null)", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({ data: null } as MockSessionData);

    const result = await getAccessToken();

    expect(result).toBeNull();
  });

  it("should return accessToken when session has valid, non-expired token", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          accessToken: "valid-token-abc",
          tokenExpiresAt: Date.now() + 300_000, // 5 minutes from now
        },
      },
    } as MockSessionData);

    const result = await getAccessToken();

    expect(result).toBe("valid-token-abc");
  });

  it("should return null when token is expired (tokenExpiresAt in the past)", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          accessToken: "expired-token",
          tokenExpiresAt: Date.now() - 120_000, // 2 minutes ago
        },
      },
    } as MockSessionData);

    const result = await getAccessToken();

    expect(result).toBeNull();
  });

  it("should return null when token expires within 60-second buffer window", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          accessToken: "almost-expired-token",
          tokenExpiresAt: Date.now() + 30_000, // 30 seconds from now (within 60s buffer)
        },
      },
    } as MockSessionData);

    const result = await getAccessToken();

    expect(result).toBeNull();
  });

  it("should return null and warn when accessToken is undefined on user object", async (): Promise<void> => {
    const warnSpy = vi.spyOn(console, "warn").mockReturnValue(undefined);
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          // no accessToken property
        },
      },
    } as MockSessionData);

    const result = await getAccessToken();

    expect(result).toBeNull();
    expect(warnSpy).toHaveBeenCalledWith("[Auth] Session exists but no accessToken found");
    warnSpy.mockRestore();
  });

  it("should return null and log error when getSession throws", async (): Promise<void> => {
    const errorSpy = vi.spyOn(console, "error").mockReturnValue(undefined);
    const sessionError = new Error("Network failure");
    vi.mocked(getSession).mockRejectedValueOnce(sessionError);

    const result = await getAccessToken();

    expect(result).toBeNull();
    expect(errorSpy).toHaveBeenCalledWith("[Auth] Failed to get access token:", sessionError);
    errorSpy.mockRestore();
  });
});

// ────────────────────────────────────────────────────────────────────────────
// AUTH-030: isAdmin tests
// ────────────────────────────────────────────────────────────────────────────

describe("isAdmin", (): void => {
  afterEach((): void => {
    vi.restoreAllMocks();
  });

  it("should return false when no session exists", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({ data: null } as MockSessionData);

    const result = await isAdmin();

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

  it("should return true when user.isAdmin is true", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "admin-1",
          isAdmin: true,
        },
      },
    } as MockSessionData);

    const result = await isAdmin();

    expect(result).toBe(true);
  });

  it("should return false when user.isAdmin is false", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          isAdmin: false,
        },
      },
    } as MockSessionData);

    const result = await isAdmin();

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

  it("should return false when user.isAdmin is undefined", async (): Promise<void> => {
    vi.mocked(getSession).mockResolvedValueOnce({
      data: {
        user: {
          id: "user-1",
          // no isAdmin property
        },
      },
    } as MockSessionData);

    const result = await isAdmin();

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

  it("should return false and log error when getSession throws", async (): Promise<void> => {
    const errorSpy = vi.spyOn(console, "error").mockReturnValue(undefined);
    const sessionError = new Error("Network failure");
    vi.mocked(getSession).mockRejectedValueOnce(sessionError);

    const result = await isAdmin();

    expect(result).toBe(false);
    expect(errorSpy).toHaveBeenCalledWith("[Auth] Failed to check admin status:", sessionError);
    errorSpy.mockRestore();
  });
});
