import { $atom, Alepha, z } from "alepha";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  ReactBrowserProvider,
  type ReactHydrationState,
  type RouterPushOptions,
  reactBrowserOptions,
} from "../providers/ReactBrowserProvider.ts";

class TestReactBrowserProvider extends ReactBrowserProvider {
  public testGetHydrationState = this.getHydrationState.bind(this);
  public testAttachAnchorInterceptor = this.attachAnchorInterceptor.bind(this);
  public testApplyHydration = this.applyHydration.bind(this);
  public pushCalls: Array<{ url: string; options?: RouterPushOptions }> = [];

  public override async push(
    url: string,
    options?: RouterPushOptions,
  ): Promise<void> {
    this.pushCalls.push({ url, options });
  }
}

describe("ReactBrowserProvider", () => {
  let alepha: Alepha;
  let provider: TestReactBrowserProvider;

  beforeEach(() => {
    alepha = Alepha.create();
    provider = alepha.inject(TestReactBrowserProvider);

    // Clean up any leftover script tags
    document.getElementById("__ssr")?.remove();
  });

  describe("getHydrationState", () => {
    it("should parse hydration data from script tag", () => {
      const data = {
        "alepha.react.router.layers": [
          { name: "home", props: { title: "Hello" } },
        ],
      };

      const script = document.createElement("script");
      script.id = "__ssr";
      script.type = "application/json";
      script.textContent = JSON.stringify(data);
      document.body.appendChild(script);

      const result = provider.testGetHydrationState();

      expect(result).toEqual(data);
      expect(result?.["alepha.react.router.layers"]).toHaveLength(1);
      expect(result?.["alepha.react.router.layers"]?.[0].name).toBe("home");
    });

    it("should return undefined when script tag is missing", () => {
      const result = provider.testGetHydrationState();

      expect(result).toBeUndefined();
    });

    it("should return undefined when script tag is empty", () => {
      const script = document.createElement("script");
      script.id = "__ssr";
      script.type = "application/json";
      script.textContent = "";
      document.body.appendChild(script);

      const result = provider.testGetHydrationState();

      expect(result).toBeUndefined();
    });

    it("should return undefined and log error on malformed JSON", () => {
      const consoleSpy = vi
        .spyOn(console, "error")
        .mockImplementation(() => {});

      const script = document.createElement("script");
      script.id = "__ssr";
      script.type = "application/json";
      script.textContent = "{invalid json";
      document.body.appendChild(script);

      const result = provider.testGetHydrationState();

      expect(result).toBeUndefined();
      expect(consoleSpy).toHaveBeenCalledOnce();

      consoleSpy.mockRestore();
    });

    it("should handle hydration data with multiple layers and atoms", () => {
      const data = {
        "alepha.react.router.layers": [
          { name: "layout", part: "/", config: { params: {} } },
          {
            name: "dashboard",
            part: "/dashboard",
            props: { count: 42 },
            config: { params: {}, query: { tab: "overview" } },
          },
        ],
        "alepha.i18n.locale": "en",
      };

      const script = document.createElement("script");
      script.id = "__ssr";
      script.type = "application/json";
      script.textContent = JSON.stringify(data);
      document.body.appendChild(script);

      const result = provider.testGetHydrationState();

      expect(result?.["alepha.react.router.layers"]).toHaveLength(2);
      expect(result?.["alepha.i18n.locale"]).toBe("en");
    });
  });

  describe("applyHydration", () => {
    const testAtom = $atom({
      name: "test.hydration.settings",
      schema: z.object({ theme: z.string() }),
      default: { theme: "light" },
    });

    it("writes a valid hydrated value through, parsed against the schema", () => {
      alepha.store.get(testAtom); // pre-register, as a real app would

      provider.testApplyHydration({
        [testAtom.key]: { theme: "dark", junk: true },
      } as unknown as ReactHydrationState);

      expect(alepha.store.get(testAtom)).toEqual({ theme: "dark" });
    });

    it("keeps the default and warns when a hydrated value fails schema validation", async () => {
      alepha.store.get(testAtom); // pre-register, as a real app would

      const onLog = vi.fn();
      alepha.events.on("log", onLog);

      provider.testApplyHydration({
        [testAtom.key]: { theme: 42 },
      } as unknown as ReactHydrationState);

      // Logger delivery is event-based and async — flush before asserting.
      await new Promise((r) => setTimeout(r, 0));

      expect(alepha.store.get(testAtom)).toEqual({ theme: "light" });
      expect(
        onLog.mock.calls.some(([payload]) =>
          payload.entry?.message?.includes(testAtom.key),
        ),
      ).toBe(true);
    });

    it("passes through a not-yet-registered atom value, decoded later at registration", () => {
      provider.testApplyHydration({
        [testAtom.key]: { theme: "dark", junk: true },
      } as unknown as ReactHydrationState);

      // First access registers the atom and decodes the raw hydrated value
      // (StateManager.register()'s decode-at-registration path).
      expect(alepha.store.get(testAtom)).toEqual({ theme: "dark" });
    });

    it("skips alepha.react.router.layers — it is not treated as an atom value", () => {
      provider.testApplyHydration({
        "alepha.react.router.layers": [{ name: "home" }],
      } as ReactHydrationState);

      expect(
        alepha.store.getAtom("alepha.react.router.layers"),
      ).toBeUndefined();
    });
  });

  describe("attachAnchorInterceptor", () => {
    let alepha: Alepha;
    let provider: TestReactBrowserProvider;
    let detach: () => void;
    let container: HTMLDivElement;

    const createAnchor = (
      attrs: Record<string, string>,
      inner?: HTMLElement,
    ): HTMLAnchorElement => {
      const a = document.createElement("a");
      for (const [k, v] of Object.entries(attrs)) {
        a.setAttribute(k, v);
      }
      if (inner) {
        a.appendChild(inner);
      } else {
        a.textContent = "link";
      }
      container.appendChild(a);
      return a;
    };

    const click = (
      target: HTMLElement,
      init: MouseEventInit = {},
    ): MouseEvent => {
      const ev = new MouseEvent("click", {
        bubbles: true,
        cancelable: true,
        button: 0,
        ...init,
      });
      target.dispatchEvent(ev);
      return ev;
    };

    beforeEach(() => {
      alepha = Alepha.create();
      provider = alepha.inject(TestReactBrowserProvider);
      container = document.createElement("div");
      document.body.appendChild(container);
      detach = provider.testAttachAnchorInterceptor();
    });

    afterEach(() => {
      detach();
      container.remove();
    });

    it("intercepts plain internal /foo anchor clicks", () => {
      const a = createAnchor({ href: "/foo" });

      const ev = click(a);

      expect(provider.pushCalls).toHaveLength(1);
      expect(provider.pushCalls[0].url).toBe("/foo");
      expect(ev.defaultPrevented).toBe(true);
    });

    it("preserves query and hash when intercepting", () => {
      const a = createAnchor({ href: "/foo?x=1#bar" });

      click(a);

      expect(provider.pushCalls[0].url).toBe("/foo?x=1#bar");
    });

    it("ignores cmd-click (metaKey)", () => {
      const a = createAnchor({ href: "/foo" });

      const ev = click(a, { metaKey: true });

      expect(provider.pushCalls).toHaveLength(0);
      expect(ev.defaultPrevented).toBe(false);
    });

    it("ignores ctrl-click", () => {
      const a = createAnchor({ href: "/foo" });

      click(a, { ctrlKey: true });

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores shift-click", () => {
      const a = createAnchor({ href: "/foo" });

      click(a, { shiftKey: true });

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores alt-click", () => {
      const a = createAnchor({ href: "/foo" });

      click(a, { altKey: true });

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores non-primary mouse buttons", () => {
      const a = createAnchor({ href: "/foo" });

      click(a, { button: 1 });

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores anchors with target='_blank'", () => {
      const a = createAnchor({ href: "/foo", target: "_blank" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores anchors with download attribute", () => {
      const a = createAnchor({ href: "/foo", download: "" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores anchors with data-no-router attribute", () => {
      const a = createAnchor({ href: "/foo", "data-no-router": "" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores mailto: hrefs", () => {
      const a = createAnchor({ href: "mailto:foo@bar.com" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores tel: hrefs", () => {
      const a = createAnchor({ href: "tel:+15555555" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores hrefs to external origins", () => {
      const a = createAnchor({ href: "https://example.com/foo" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("ignores hash-only #section hrefs", () => {
      const a = createAnchor({ href: "#section" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("intercepts when click target is nested inside the anchor", () => {
      const span = document.createElement("span");
      span.textContent = "inner";
      createAnchor({ href: "/foo" }, span);

      click(span);

      expect(provider.pushCalls).toHaveLength(1);
      expect(provider.pushCalls[0].url).toBe("/foo");
    });

    it("skips when defaultPrevented is already true", () => {
      const a = createAnchor({ href: "/foo" });
      a.addEventListener("click", (ev) => ev.preventDefault());

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });

    it("respects interceptAnchorClicks=false at runtime", () => {
      alepha.store.set(reactBrowserOptions.key, {
        ...alepha.store.get(reactBrowserOptions.key)!,
        interceptAnchorClicks: false,
      });
      const a = createAnchor({ href: "/foo" });

      click(a);

      expect(provider.pushCalls).toHaveLength(0);
    });
  });

  /**
   * `url` is what `push()` compares the committed route against and what gets
   * written back to history. Dropping the fragment here made every internal
   * `<a href="/docs#section">` land on /docs with no anchor, never scrolling.
   */
  describe("url", () => {
    it("keeps the hash fragment", () => {
      window.history.pushState({}, "", "/docs#section");

      expect(provider.url).toBe("/docs#section");
    });

    it("keeps the hash alongside the query string", () => {
      window.history.pushState({}, "", "/docs?q=1#section");

      expect(provider.url).toBe("/docs?q=1#section");
    });

    it("is unchanged when there is no hash", () => {
      window.history.pushState({}, "", "/docs?q=1");

      expect(provider.url).toBe("/docs?q=1");
    });
  });
});
