import { describe, it, expect, vi, beforeEach } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import { renderWithProviders } from "@repo/core-testing/react";
import { screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InMemoryConsent } from "@repo/core-consent";
import { ConsentProvider } from "@repo/core-consent/react";
import { CookieConsentBanner } from "./cookie-consent-banner";
import { CookieConsentBannerLoader } from "./cookie-consent-banner-loader";
import {
  CONSENT_COOKIE_NAME,
  readConsentCookie,
  writeConsentCookie,
  clearConsentCookie,
} from "./cookie-helpers";
import type { ConsentCookieState } from "./types";

expect.extend(toHaveNoViolations);

beforeEach(() => {
  // Clear the consent cookie before each test
  document.cookie = `${CONSENT_COOKIE_NAME}=; Max-Age=0; Path=/`;
});

// ---------------------------------------------------------------------------
// Cookie helpers
// ---------------------------------------------------------------------------
describe("readConsentCookie", () => {
  it("returns null when no cookie is set", () => {
    expect(readConsentCookie()).toBeNull();
  });

  it("reads back a cookie written by writeConsentCookie", () => {
    const state: ConsentCookieState = {
      _v: 1,
      categories: { essential: true, analytics: false, marketing: false },
    };
    writeConsentCookie(state);
    expect(readConsentCookie()).toEqual(state);
  });

  it("returns null for malformed JSON", () => {
    document.cookie = `${CONSENT_COOKIE_NAME}=notjson`;
    expect(readConsentCookie()).toBeNull();
  });

  it("returns null when _v does not match COOKIE_VERSION", () => {
    const bad = encodeURIComponent(JSON.stringify({ _v: 99, categories: {} }));
    document.cookie = `${CONSENT_COOKIE_NAME}=${bad}`;
    expect(readConsentCookie()).toBeNull();
  });

  it("returns null when categories key is missing", () => {
    const bad = encodeURIComponent(JSON.stringify({ _v: 1 }));
    document.cookie = `${CONSENT_COOKIE_NAME}=${bad}`;
    expect(readConsentCookie()).toBeNull();
  });

  it("returns null when categories is not an object", () => {
    const bad = encodeURIComponent(
      JSON.stringify({ _v: 1, categories: "bad" }),
    );
    document.cookie = `${CONSENT_COOKIE_NAME}=${bad}`;
    expect(readConsentCookie()).toBeNull();
  });
});

describe("clearConsentCookie", () => {
  it("removes the cookie so readConsentCookie returns null", () => {
    writeConsentCookie({ _v: 1, categories: { essential: true } });
    clearConsentCookie();
    expect(readConsentCookie()).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — Reject All
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — Reject All", () => {
  it("fires onConsentChange with all non-essential categories denied", async () => {
    const onConsentChange = vi.fn();
    renderWithProviders(
      <CookieConsentBanner variant="modal" onConsentChange={onConsentChange} />,
    );

    await userEvent.click(screen.getByRole("button", { name: "Reject All" }));

    await waitFor(() => expect(onConsentChange).toHaveBeenCalledOnce());
    const [states] = onConsentChange.mock.calls[0] as [
      Array<{ category: string; state: string }>,
    ];

    const find = (id: string) => states.find((s) => s.category === id);

    expect(find("essential")?.state).toBe("granted"); // required — always granted
    expect(find("functional")?.state).toBe("denied");
    expect(find("analytics")?.state).toBe("denied");
    expect(find("marketing")?.state).toBe("denied");
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — Save Selected with toggle
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — Save Selected with toggle", () => {
  it("fires onConsentChange with analytics granted after toggling it on", async () => {
    const onConsentChange = vi.fn();
    renderWithProviders(
      <CookieConsentBanner variant="modal" onConsentChange={onConsentChange} />,
    );

    // Toggle analytics ON (starts off by default)
    await userEvent.click(
      screen.getByRole("switch", { name: "Analytics cookies" }),
    );
    await userEvent.click(
      screen.getByRole("button", { name: "Save Selected" }),
    );

    await waitFor(() => expect(onConsentChange).toHaveBeenCalledOnce());
    const [states] = onConsentChange.mock.calls[0] as [
      Array<{ category: string; state: string }>,
    ];

    expect(states.find((s) => s.category === "analytics")?.state).toBe(
      "granted",
    );
    // Other non-essential categories remain denied
    expect(states.find((s) => s.category === "marketing")?.state).toBe(
      "denied",
    );
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — ESC key
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — ESC key", () => {
  it("ESC in modal triggers Reject All semantics", async () => {
    const onConsentChange = vi.fn();
    renderWithProviders(
      <CookieConsentBanner variant="modal" onConsentChange={onConsentChange} />,
    );

    fireEvent.keyDown(document, { key: "Escape" });

    await waitFor(() => expect(onConsentChange).toHaveBeenCalledOnce());
    const [states] = onConsentChange.mock.calls[0] as [
      Array<{ category: string; state: string }>,
    ];
    expect(states.find((s) => s.category === "analytics")?.state).toBe(
      "denied",
    );
  });

  it("ESC does not trigger Reject All for banner variant", async () => {
    const onConsentChange = vi.fn();
    renderWithProviders(
      <CookieConsentBanner
        variant="banner"
        onConsentChange={onConsentChange}
      />,
    );

    fireEvent.keyDown(document, { key: "Escape" });

    // Give async handler a chance to run if it were registered
    await new Promise((r) => setTimeout(r, 10));
    expect(onConsentChange).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — Tab order
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — Tab order", () => {
  it("Reject All precedes Accept All in DOM order (CNIL equal-prominence)", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);

    const rejectBtn = screen.getByRole("button", { name: "Reject All" });
    const acceptBtn = screen.getByRole("button", { name: "Accept All" });

    // DOCUMENT_POSITION_FOLLOWING (4) means acceptBtn follows rejectBtn in the DOM
    expect(
      rejectBtn.compareDocumentPosition(acceptBtn) &
        Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();
  });

  it("Reject All precedes Accept All in banner variant too", () => {
    renderWithProviders(<CookieConsentBanner variant="banner" />);

    const rejectBtn = screen.getByRole("button", { name: "Reject All" });
    const acceptBtn = screen.getByRole("button", { name: "Accept All" });

    expect(
      rejectBtn.compareDocumentPosition(acceptBtn) &
        Node.DOCUMENT_POSITION_FOLLOWING,
    ).toBeTruthy();
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — Modal focus trap
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — Modal focus trap", () => {
  it("Tab on last focusable element wraps focus to first", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);

    const dialog = screen.getByRole("dialog");
    const focusable = Array.from(
      dialog.querySelectorAll<HTMLElement>(
        'button:not(:disabled), [href], input:not(:disabled), [tabindex]:not([tabindex="-1"])',
      ),
    );

    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    expect(first).toBeDefined();
    expect(last).toBeDefined();

    last!.focus();
    fireEvent.keyDown(document, { key: "Tab", shiftKey: false });

    expect(document.activeElement).toBe(first);
  });

  it("Shift+Tab on first focusable element wraps focus to last", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);

    const dialog = screen.getByRole("dialog");
    const focusable = Array.from(
      dialog.querySelectorAll<HTMLElement>(
        'button:not(:disabled), [href], input:not(:disabled), [tabindex]:not([tabindex="-1"])',
      ),
    );

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    first!.focus();
    fireEvent.keyDown(document, { key: "Tab", shiftKey: true });

    expect(document.activeElement).toBe(last);
  });

  it("focus is contained within the dialog after Tab on last element", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);

    const dialog = screen.getByRole("dialog");
    const focusable = Array.from(
      dialog.querySelectorAll<HTMLElement>(
        'button:not(:disabled), [href], input:not(:disabled), [tabindex]:not([tabindex="-1"])',
      ),
    );
    focusable[focusable.length - 1]!.focus();
    fireEvent.keyDown(document, { key: "Tab", shiftKey: false });

    expect(dialog.contains(document.activeElement)).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — render-prop overrides
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — render-prop overrides", () => {
  it("renderHeader replaces the default heading", () => {
    renderWithProviders(
      <CookieConsentBanner
        variant="modal"
        renderHeader={({ title }) => (
          <div data-testid="custom-header">{title}</div>
        )}
      />,
    );

    expect(screen.getByTestId("custom-header")).toBeInTheDocument();
    expect(screen.queryByRole("heading")).not.toBeInTheDocument();
  });

  it("renderActions replaces the default action buttons", () => {
    renderWithProviders(
      <CookieConsentBanner
        variant="modal"
        renderActions={({ onRejectAll: reject }) => (
          <button onClick={reject} data-testid="custom-reject">
            Custom Reject
          </button>
        )}
      />,
    );

    const customReject = screen.getByTestId("custom-reject");
    expect(customReject).toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: "Reject All" }),
    ).not.toBeInTheDocument();
  });

  it("renderCategoryRow replaces the default category row", () => {
    renderWithProviders(
      <CookieConsentBanner
        variant="modal"
        renderCategoryRow={({ category }) => (
          <div data-testid={`custom-row-${category.id}`}>{category.label}</div>
        )}
      />,
    );

    expect(screen.getByTestId("custom-row-analytics")).toBeInTheDocument();
    expect(screen.queryByRole("switch")).not.toBeInTheDocument();
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — Accept All
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — Accept All", () => {
  it("fires onConsentChange with all categories granted", async () => {
    const onConsentChange = vi.fn();
    renderWithProviders(
      <CookieConsentBanner variant="modal" onConsentChange={onConsentChange} />,
    );

    await userEvent.click(screen.getByRole("button", { name: "Accept All" }));

    await waitFor(() => expect(onConsentChange).toHaveBeenCalledOnce());
    const [states] = onConsentChange.mock.calls[0] as [
      Array<{ category: string; state: string }>,
    ];

    expect(states.every((s) => s.state === "granted")).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — ARIA roles
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — ARIA roles", () => {
  it("modal variant renders a dialog element", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);
    expect(screen.getByRole("dialog")).toBeInTheDocument();
  });

  it("banner variant renders a region element", () => {
    renderWithProviders(<CookieConsentBanner variant="banner" />);
    expect(
      screen.getByRole("region", { name: "Cookie Preferences" }),
    ).toBeInTheDocument();
  });

  it("essential toggle is disabled (non-toggleable)", () => {
    renderWithProviders(<CookieConsentBanner variant="modal" />);
    const essential = screen.getByRole("switch", { name: "Essential cookies" });
    expect(essential).toBeDisabled();
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — cookie initialisation
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — cookie initialisation", () => {
  it("initialises toggles from an existing __consent_state cookie", () => {
    writeConsentCookie({
      _v: 1,
      categories: {
        essential: true,
        analytics: true,
        functional: false,
        marketing: false,
      },
    });

    renderWithProviders(<CookieConsentBanner variant="modal" />);

    expect(
      screen.getByRole("switch", { name: "Analytics cookies" }),
    ).toHaveAttribute("aria-checked", "true");
    expect(
      screen.getByRole("switch", { name: "Functional cookies" }),
    ).toHaveAttribute("aria-checked", "false");
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — ConsentProvider integration
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — ConsentProvider integration", () => {
  it("reads initial state from consent when inside ConsentProvider (no cookie)", async () => {
    const consent = new InMemoryConsent();
    await consent.grant("analytics");

    renderWithProviders(
      <ConsentProvider value={consent}>
        <CookieConsentBanner variant="modal" />
      </ConsentProvider>,
    );

    // analytics was pre-granted, should show as checked
    expect(
      screen.getByRole("switch", { name: "Analytics cookies" }),
    ).toHaveAttribute("aria-checked", "true");
  });

  it("calls consent.grant for each granted category on Accept All", async () => {
    const consent = new InMemoryConsent();
    const grantSpy = vi.spyOn(consent, "grant");
    const onConsentChange = vi.fn();

    renderWithProviders(
      <ConsentProvider value={consent}>
        <CookieConsentBanner
          variant="modal"
          onConsentChange={onConsentChange}
        />
      </ConsentProvider>,
    );

    await userEvent.click(screen.getByRole("button", { name: "Accept All" }));

    await waitFor(() => expect(onConsentChange).toHaveBeenCalledOnce());
    expect(grantSpy).toHaveBeenCalledWith(
      "analytics",
      expect.objectContaining({ method: "banner-accept" }),
    );
  });

  it("calls consent.withdraw for denied categories on Reject All", async () => {
    const consent = new InMemoryConsent();
    await consent.grant("analytics"); // pre-grant so withdraw is called
    const withdrawSpy = vi.spyOn(consent, "withdraw");

    renderWithProviders(
      <ConsentProvider value={consent}>
        <CookieConsentBanner variant="modal" />
      </ConsentProvider>,
    );

    await userEvent.click(screen.getByRole("button", { name: "Reject All" }));

    await waitFor(() => expect(withdrawSpy).toHaveBeenCalledWith("analytics"));
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBannerLoader
// ---------------------------------------------------------------------------
describe("CookieConsentBannerLoader", () => {
  it("mounts the banner client-side after hydration", async () => {
    renderWithProviders(<CookieConsentBannerLoader variant="modal" />);

    // After mounting (useEffect fires), the banner should appear
    await waitFor(() => {
      expect(screen.getByRole("dialog")).toBeInTheDocument();
    });
  });
});

// ---------------------------------------------------------------------------
// CookieConsentBanner — axe a11y (WCAG 2.2 AA)
// ---------------------------------------------------------------------------
describe("CookieConsentBanner — axe a11y", () => {
  it("modal variant has no axe violations", async () => {
    const { container } = renderWithProviders(
      <CookieConsentBanner variant="modal" />,
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it("banner variant has no axe violations", async () => {
    const { container } = renderWithProviders(
      <CookieConsentBanner variant="banner" />,
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});
