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

const { mockApi, mockOptima, mockIsInvalidSignatureError, mockRedirect } =
  vi.hoisted(() => ({
    mockApi: {
      get: vi.fn(),
    },
    mockOptima: {
      auth: {},
      user: {
        isLoggedIn: vi.fn(),
        isLoggedInServer: vi.fn(),
        logout: vi.fn(),
        refreshSession: vi.fn(),
        fetchInfo: vi.fn(),
        checkPermissions: vi.fn(),
      },
    },
    mockIsInvalidSignatureError: vi.fn(),
    mockRedirect: vi.fn(),
  }));

vi.mock("$lib", () => ({ optima: mockOptima }));
vi.mock("$lib/optima-api/axios", () => ({ default: mockApi, api: mockApi }));
vi.mock("$lib/optima-api/errorHandler", () => ({
  isInvalidSignatureError: mockIsInvalidSignatureError,
}));
vi.mock("@sveltejs/kit", () => ({
  redirect: mockRedirect,
}));

import { handle } from "./hooks.server";

function createMockEvent(overrides: Record<string, unknown> = {}) {
  const cookies: Record<string, string> = {};
  return {
    url: new URL("http://localhost/"),
    cookies: {
      get: vi.fn((name: string) => cookies[name] ?? null),
      set: vi.fn((name: string, value: string) => {
        cookies[name] = value;
      }),
      delete: vi.fn((name: string) => {
        delete cookies[name];
      }),
    },
    locals: {} as Record<string, unknown>,
    ...overrides,
  };
}

function createResolve(response = new Response("OK")) {
  return vi.fn().mockResolvedValue(response);
}

describe("hooks.server handle", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockIsInvalidSignatureError.mockReturnValue(false);
  });

  // ── Health check bypass ───────────────────────────────────────────

  it("passes /healthz through without API check", async () => {
    const event = createMockEvent({
      url: new URL("http://localhost/healthz"),
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(mockApi.get).not.toHaveBeenCalled();
    expect(resolve).toHaveBeenCalledWith(event);
  });

  // ── API health check ─────────────────────────────────────────────

  it("returns 503 page when API teapot check fails", async () => {
    mockApi.get.mockRejectedValueOnce(new Error("Network error"));
    const event = createMockEvent();
    const resolve = createResolve();

    const response = await handle({ event, resolve } as any);

    expect(response.status).toBe(503);
    const html = await response.text();
    expect(html).toContain("Unable to Reach API");
  });

  it("returns 503 when API teapot returns non-418 status", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 200 });
    const event = createMockEvent();
    const resolve = createResolve();

    const response = await handle({ event, resolve } as any);

    expect(response.status).toBe(503);
  });

  // ── Logout path ──────────────────────────────────────────────────

  it("clears cookies and redirects on /logout", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    const event = createMockEvent({
      url: new URL("http://localhost/logout"),
    });
    event.cookies.get = vi
      .fn()
      .mockReturnValueOnce("access-tok")
      .mockReturnValueOnce("refresh-tok");
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(event.cookies.delete).toHaveBeenCalledWith("accessToken", {
      path: "/",
    });
    expect(event.cookies.delete).toHaveBeenCalledWith("refreshToken", {
      path: "/",
    });
    expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
  });

  // ── Login path when already logged in ─────────────────────────────

  it("redirects to / when visiting /login while already logged in", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockOptima.user.isLoggedInServer.mockReturnValue(true);
    const event = createMockEvent({
      url: new URL("http://localhost/login"),
    });
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(mockRedirect).toHaveBeenCalledWith(303, "/");
  });

  // ── Login path when not logged in ─────────────────────────────────

  it("resolves /login normally when user is not logged in", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockOptima.user.isLoggedInServer.mockReturnValue(false);
    const event = createMockEvent({
      url: new URL("http://localhost/login"),
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(resolve).toHaveBeenCalledWith(event);
  });

  // ── No tokens — force logout ──────────────────────────────────────

  it("forces logout when no access or refresh tokens exist", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn().mockReturnValue(null);
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(mockOptima.user.logout).toHaveBeenCalled();
    expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
  });

  // ── Valid access token — normal flow ──────────────────────────────

  it("resolves normally with valid non-expired access token", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    // Create a JWT payload with exp far in the future
    const payload = {
      exp: Math.floor(Date.now() / 1000) + 3600,
      sub: "user-1",
    };
    const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
      "base64url"
    );
    const fakeJwt = `header.${encodedPayload}.signature`;

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return fakeJwt;
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(resolve).toHaveBeenCalledWith(event);
    // setTokens should have set session on locals
    expect(event.locals.session).toBeDefined();
  });

  // ── Expired access token — refresh ────────────────────────────────

  it("refreshes expired access token using refresh token", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    // Create an expired JWT
    const payload = { exp: Math.floor(Date.now() / 1000) - 100, sub: "u1" };
    const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
      "base64url"
    );
    const fakeJwt = `h.${encodedPayload}.s`;

    mockOptima.user.refreshSession.mockResolvedValueOnce({
      accessToken: "new-access",
      refreshToken: "new-refresh",
    });

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return fakeJwt;
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
    expect(event.cookies.set).toHaveBeenCalledWith(
      "accessToken",
      "new-access",
      { path: "/" }
    );
    expect(resolve).toHaveBeenCalledWith(event);
  });

  // ── Expired token, no refresh token — force logout ────────────────

  it("forces logout when token expired and no refresh token", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    const payload = { exp: Math.floor(Date.now() / 1000) - 100, sub: "u1" };
    const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
      "base64url"
    );
    const fakeJwt = `h.${encodedPayload}.s`;

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return fakeJwt;
      if (name === "refreshToken") return null;
      return null;
    });
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(mockOptima.user.logout).toHaveBeenCalled();
    expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
  });

  // ── Invalid signature error — force logout ────────────────────────

  it("forces logout on invalid signature error during token decode", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockIsInvalidSignatureError.mockReturnValue(true);

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    // Return a malformed JWT that will throw when parsed
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return "bad.!!!.token";
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(mockOptima.user.logout).toHaveBeenCalled();
    expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
  });

  // ── Malformed token, refresh succeeds ─────────────────────────────

  it("refreshes when access token is malformed and refresh token exists", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockIsInvalidSignatureError.mockReturnValue(false);
    mockOptima.user.refreshSession.mockResolvedValueOnce({
      accessToken: "recovered-access",
      refreshToken: "recovered-refresh",
    });

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return "totally.broken";
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
    expect(resolve).toHaveBeenCalled();
  });

  // ── No access token, has refresh — try refresh ────────────────────

  it("attempts refresh when only refresh token exists", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockOptima.user.refreshSession.mockResolvedValueOnce({
      accessToken: "new-access",
      refreshToken: "new-refresh",
    });

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return null;
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    await handle({ event, resolve } as any);

    expect(mockOptima.user.refreshSession).toHaveBeenCalledWith("refresh-tok");
    expect(resolve).toHaveBeenCalled();
  });

  // ── No access token, refresh fails — force logout ─────────────────

  it("forces logout when only refresh token exists but refresh fails", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    mockIsInvalidSignatureError.mockReturnValue(false);
    mockOptima.user.refreshSession.mockRejectedValueOnce(
      new Error("Refresh failed")
    );

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return null;
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(mockOptima.user.logout).toHaveBeenCalled();
    expect(mockRedirect).toHaveBeenCalledWith(303, "/login");
  });

  // ── Refresh fails with invalid signature ──────────────────────────

  it("logs warning on invalid signature during refresh fallback", async () => {
    mockApi.get.mockResolvedValueOnce({ status: 418 });
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

    // First call: malformed token parse error (not invalid signature)
    // Second call: refresh also fails with invalid signature
    mockIsInvalidSignatureError
      .mockReturnValueOnce(false)
      .mockReturnValueOnce(true);
    mockOptima.user.refreshSession.mockRejectedValueOnce(
      new Error("invalid signature")
    );

    const event = createMockEvent({
      url: new URL("http://localhost/dashboard"),
    });
    event.cookies.get = vi.fn((name: string) => {
      if (name === "accessToken") return "bad.!!!.token";
      if (name === "refreshToken") return "refresh-tok";
      return null;
    });
    const resolve = createResolve();

    try {
      await handle({ event, resolve } as any);
    } catch {
      // redirect throws
    }

    expect(warnSpy).toHaveBeenCalledWith(
      "Invalid refresh token signature — forcing logout."
    );
    expect(mockOptima.user.logout).toHaveBeenCalled();
    warnSpy.mockRestore();
  });
});
