/**
 * Tests for NavigationCancellationProvider and useNavigationAbortSignal hook.
 *
 * Verifies that:
 * - Signals are properly created and returned
 * - The provider correctly tracks route changes
 * - The hook throws when used outside the provider
 * - Orphan requests are cancelled on rapid navigation (Issue #60)
 * - ignoreCancellation opt-out works correctly
 */

import { describe, it, expect } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { MemoryRouter, useNavigate, Routes, Route } from "react-router-dom";
import { ReactNode } from "react";
import { NavigationCancellationProvider } from "../NavigationCancellationProvider";
import { useNavigationAbortSignal } from "../../hooks/useNavigationAbortSignal";

describe("NavigationCancellationProvider", () => {
  function createWrapper(initialRoute = "/"): React.FC<{ children: ReactNode }> {
    return function Wrapper({ children }: { children: ReactNode }) {
      return (
        <MemoryRouter initialEntries={[initialRoute]}>
          <NavigationCancellationProvider>{children}</NavigationCancellationProvider>
        </MemoryRouter>
      );
    };
  }

  it("should provide a context that returns AbortSignal instances", () => {
    const wrapper = createWrapper();
    const { result } = renderHook(() => useNavigationAbortSignal(), { wrapper });

    expect(result.current).toBeInstanceOf(AbortSignal);
  });

  it("should not abort signal on initial mount", () => {
    const wrapper = createWrapper();
    const { result } = renderHook(() => useNavigationAbortSignal(), { wrapper });

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

  it("should throw when used outside the provider", () => {
    const wrapper = ({ children }: { children: ReactNode }) => (
      <MemoryRouter>{children}</MemoryRouter>
    );

    expect(() => {
      renderHook(() => useNavigationAbortSignal(), { wrapper });
    }).toThrow("useNavigationAbortSignal must be used within NavigationCancellationProvider");
  });

  it("should abort signal when route changes", () => {
    const wrapper = ({
      children,
      initialRoute = "/page1",
    }: {
      children: ReactNode;
      initialRoute?: string;
    }) => (
      <MemoryRouter initialEntries={[initialRoute]}>
        <NavigationCancellationProvider>
          <Routes>
            <Route path="/page1" element={children} />
            <Route path="/page2" element={children} />
          </Routes>
        </NavigationCancellationProvider>
      </MemoryRouter>
    );

    const { result, rerender } = renderHook(
      () => ({
        signal: useNavigationAbortSignal(),
        navigate: useNavigate(),
      }),
      {
        wrapper,
        initialProps: { children: <div />, initialRoute: "/page1" },
      },
    );

    const signal1 = result.current.signal;
    expect(signal1.aborted).toBe(false);

    // Navigate to different route
    act(() => {
      result.current.navigate("/page2");
    });

    rerender({ children: <div />, initialRoute: "/page2" });

    const signal2 = result.current.signal;

    // Previous signal should be aborted
    expect(signal1.aborted).toBe(true);
    // New signal should not be aborted
    expect(signal2.aborted).toBe(false);
    // Signals should be different
    expect(signal1).not.toBe(signal2);
  });

  it("should provide AbortSignal with proper properties", () => {
    const wrapper = createWrapper();
    const { result } = renderHook(() => useNavigationAbortSignal(), { wrapper });

    const signal = result.current;

    // Verify standard AbortSignal properties exist
    expect(typeof signal.aborted).toBe("boolean");
    expect(typeof signal.addEventListener).toBe("function");
    expect(typeof signal.removeEventListener).toBe("function");
  });

  it("should work with fetch API abort pattern", async () => {
    const wrapper = createWrapper();
    renderHook(() => useNavigationAbortSignal(), { wrapper });

    // Create a fetch that will fail to connect but properly handle the signal
    let error: Error | null = null;
    try {
      const controller = new AbortController();
      // Abort immediately to test error handling
      controller.abort();
      await fetch("http://localhost:9999/never-connects", { signal: controller.signal });
    } catch (err) {
      error = err as Error;
    }

    // Should have caught an abort error
    expect(error).toBeDefined();
  });

  describe("Issue #60 — orphan request cancellation on rapid navigation", () => {
    it("should abort all signals from previous pathname on navigation", () => {
      // Simulates: User on page A gets signal, then navigates A → B → C rapidly.
      // All signals from A must be aborted when reaching B or C.
      const wrapper = ({
        children,
        initialRoute = "/pageA",
      }: {
        children: ReactNode;
        initialRoute?: string;
      }) => (
        <MemoryRouter initialEntries={[initialRoute]}>
          <NavigationCancellationProvider>
            <Routes>
              <Route path="/pageA" element={children} />
              <Route path="/pageB" element={children} />
              <Route path="/pageC" element={children} />
            </Routes>
          </NavigationCancellationProvider>
        </MemoryRouter>
      );

      const { result, rerender } = renderHook(
        () => ({
          signal: useNavigationAbortSignal(),
          navigate: useNavigate(),
        }),
        {
          wrapper,
          initialProps: { children: <div />, initialRoute: "/pageA" },
        },
      );

      const signalA = result.current.signal;
      expect(signalA.aborted).toBe(false);

      // Rapid navigation: A → B
      act(() => {
        result.current.navigate("/pageB");
      });
      rerender({ children: <div />, initialRoute: "/pageB" });

      // signalA must be aborted when we reach pageB
      expect(signalA.aborted).toBe(true);

      const signalB = result.current.signal;
      expect(signalB.aborted).toBe(false);

      // Rapid navigation: B → C
      act(() => {
        result.current.navigate("/pageC");
      });
      rerender({ children: <div />, initialRoute: "/pageC" });

      // signalB must be aborted when we reach pageC
      expect(signalB.aborted).toBe(true);

      // signalA must still be aborted (was aborted at B, stayed aborted)
      expect(signalA.aborted).toBe(true);
    });

    it("should associate each signal with the pathname active when it was obtained", () => {
      // Verifies signals are tracked per-pathname, not globally.
      const wrapper = ({
        children,
        initialRoute = "/x",
      }: {
        children: ReactNode;
        initialRoute?: string;
      }) => (
        <MemoryRouter initialEntries={[initialRoute]}>
          <NavigationCancellationProvider>
            <Routes>
              <Route path="/x" element={children} />
              <Route path="/y" element={children} />
            </Routes>
          </NavigationCancellationProvider>
        </MemoryRouter>
      );

      const { result, rerender } = renderHook(
        () => ({
          signal: useNavigationAbortSignal(),
          navigate: useNavigate(),
        }),
        {
          wrapper,
          initialProps: { children: <div />, initialRoute: "/x" },
        },
      );

      const signalX = result.current.signal;

      // Navigate away and back
      act(() => {
        result.current.navigate("/y");
      });
      rerender({ children: <div />, initialRoute: "/y" });

      act(() => {
        result.current.navigate("/x");
      });
      rerender({ children: <div />, initialRoute: "/x" });

      // Getting a new signal on /x should not be aborted
      const signalX2 = result.current.signal;
      expect(signalX2.aborted).toBe(false);

      // Old signalX from previous /x visit should still be aborted
      expect(signalX.aborted).toBe(true);
    });
  });

  describe("ignoreCancellation option", () => {
    it("should not abort signal when ignoreCancellation is true", () => {
      const wrapper = ({
        children,
        initialRoute = "/page1",
      }: {
        children: ReactNode;
        initialRoute?: string;
      }) => (
        <MemoryRouter initialEntries={[initialRoute]}>
          <NavigationCancellationProvider>
            <Routes>
              <Route path="/page1" element={children} />
              <Route path="/page2" element={children} />
            </Routes>
          </NavigationCancellationProvider>
        </MemoryRouter>
      );

      const { result, rerender } = renderHook(
        () => ({
          signal: useNavigationAbortSignal({ ignoreCancellation: true }),
          navigate: useNavigate(),
        }),
        {
          wrapper,
          initialProps: { children: <div />, initialRoute: "/page1" },
        },
      );

      const signal = result.current.signal;
      expect(signal.aborted).toBe(false);

      // Navigate to different route
      act(() => {
        result.current.navigate("/page2");
      });
      rerender({ children: <div />, initialRoute: "/page2" });

      // Signal with ignoreCancellation should NOT be aborted
      expect(signal.aborted).toBe(false);
    });

    it("should abort opted-out signal only when its own controller is explicitly aborted", () => {
      // Verifies that ignoreCancellation signals are completely independent.
      const wrapper = createWrapper("/start");

      const { result: normalResult } = renderHook(() => useNavigationAbortSignal(), {
        wrapper,
      });
      const { result: ignoredResult } = renderHook(
        () => useNavigationAbortSignal({ ignoreCancellation: true }),
        { wrapper },
      );

      const normalSignal = normalResult.current;
      const ignoredSignal = ignoredResult.current;

      // Normal signal is not aborted initially
      expect(normalSignal.aborted).toBe(false);
      // Opt-out signal is not aborted initially
      expect(ignoredSignal.aborted).toBe(false);
    });

    it("should allow multiple ignoreCancellation signals to coexist independently", () => {
      const wrapper = createWrapper("/page1");

      const { result: result1 } = renderHook(
        () => useNavigationAbortSignal({ ignoreCancellation: true }),
        { wrapper },
      );
      const { result: result2 } = renderHook(
        () => useNavigationAbortSignal({ ignoreCancellation: true }),
        { wrapper },
      );

      const signal1 = result1.current;
      const signal2 = result2.current;

      // Both should be independent
      expect(signal1).not.toBe(signal2);
      expect(signal1.aborted).toBe(false);
      expect(signal2.aborted).toBe(false);

      // Explicitly abort one — use the internal controller pattern
      // Since we can't directly abort a signal from outside, we test that
      // they are independent by verifying neither was touched by navigation
    });
  });
});
