/**
 * @vitest-environment jsdom
 */

import { describe, it, expect, beforeEach, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useSearchHistory } from "./useSearchHistory";
import type { SearchHistoryItem } from "./useSearchHistory";

// ---------------------------------------------------------------------------
// sessionStorage mock (TZ §4.1.9.5: session-scoped — cleared on reload)
// ---------------------------------------------------------------------------

function createMockSessionStorage() {
  const store = new Map<string, string>();
  return {
    getItem: vi.fn((key: string) => store.get(key) ?? null),
    setItem: vi.fn((key: string, value: string) => {
      store.set(key, value);
    }),
    removeItem: vi.fn((key: string) => {
      store.delete(key);
    }),
    get length() {
      return store.size;
    },
    key: vi.fn((index: number) => [...store.keys()][index] ?? null),
    clear: vi.fn(() => store.clear()),
    store,
  } satisfies Storage & { store: Map<string, string> };
}

// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------

const flightItem: SearchHistoryItem = {
  type: "flight-number",
  url: "/ru/onlineboard/flight/SU0001-20250528",
  label: "SU 0001",
};

const routeItem: SearchHistoryItem = {
  type: "board-route",
  url: "/ru/onlineboard/route/SVO-JFK-20250528",
  label: "SVO - JFK",
};

const scheduleItem: SearchHistoryItem = {
  type: "schedule-route",
  url: "/ru/schedule/SVO-LED-20260601-20260607",
  label: "SVO — LED",
  params: {
    departure: "SVO",
    arrival: "LED",
    dateFrom: "20260601",
    dateTo: "20260607",
  },
};

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("useSearchHistory", () => {
  let mockStorage: ReturnType<typeof createMockSessionStorage>;

  beforeEach(() => {
    mockStorage = createMockSessionStorage();
    vi.stubGlobal("sessionStorage", mockStorage);
  });

  it("starts with empty history when nothing in storage", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));
    expect(result.current.items).toEqual([]);
  });

  it("loads existing history from storage on mount", () => {
    mockStorage.store.set(
      "afl_history_ru",
      JSON.stringify([flightItem]),
    );

    const { result } = renderHook(() => useSearchHistory("ru"));
    expect(result.current.items).toEqual([flightItem]);
  });

  it("adds an item and persists to storage", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
    });

    expect(result.current.items).toEqual([flightItem]);
    expect(mockStorage.setItem).toHaveBeenCalledWith(
      "afl_history_ru",
      JSON.stringify([flightItem]),
    );
  });

  it("prepends new items (most recent first)", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
    });
    act(() => {
      result.current.add(routeItem);
    });

    expect(result.current.items).toEqual([routeItem, flightItem]);
  });

  // TZ §4.1.9.5: dedup — same search must not appear twice
  it("4.1.9.5-R: deduplicates by URL — skips if most recent matches", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
    });
    act(() => {
      result.current.add(flightItem);
    });

    expect(result.current.items).toEqual([flightItem]);
  });

  // TZ §4.1.9.5: re-running a saved search pushes it to top
  it("4.1.9.5-R: re-running an older search moves it to top (dedup + bump)", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
    });
    act(() => {
      result.current.add(routeItem);
    });
    act(() => {
      result.current.add(flightItem);
    });

    expect(result.current.items).toEqual([flightItem, routeItem]);
  });

  // TIRREDESIGN-5 / TZ §4.1.14.3.8: flight-number history keeps at most 8
  // entries independently of board / schedule sections (one section
  // filling up must not evict entries from the others).
  it("caps flight-number section at 8 most-recent items", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    for (let i = 0; i < 12; i++) {
      act(() => {
        result.current.add({
          type: "flight-number",
          url: `/flight-${i}`,
          label: `Flight ${i}`,
        });
      });
    }

    expect(result.current.items).toHaveLength(8);
    expect(result.current.items[0]?.url).toBe("/flight-11");
    expect(result.current.items[7]?.url).toBe("/flight-4");
  });

  it("caps each section independently (board + schedule + flight-number)", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    for (let i = 0; i < 10; i++) {
      act(() => {
        result.current.add({
          type: "flight-number",
          url: `/flight-${i}`,
          label: `Flight ${i}`,
        });
      });
      act(() => {
        result.current.add({
          type: "board-route",
          url: `/departure-${i}`,
          label: `Departure ${i}`,
        });
      });
      act(() => {
        result.current.add({
          type: "schedule-route",
          url: `/schedule-${i}`,
          label: `Schedule ${i}`,
        });
      });
    }

    const byType = (t: string) =>
      result.current.items.filter((item) => item.type === t);
    expect(byType("flight-number")).toHaveLength(8);
    expect(byType("board-route")).toHaveLength(8);
    expect(byType("schedule-route")).toHaveLength(8);
  });

  it("namespaces by language — different langs have separate history", () => {
    const { result: ruResult } = renderHook(() => useSearchHistory("ru"));
    const { result: enResult } = renderHook(() => useSearchHistory("en"));

    act(() => {
      ruResult.current.add(flightItem);
    });

    expect(ruResult.current.items).toEqual([flightItem]);
    expect(enResult.current.items).toEqual([]);
  });

  it("clear removes all items and deletes from storage", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
      result.current.add(routeItem);
    });
    act(() => {
      result.current.clear();
    });

    expect(result.current.items).toEqual([]);
    expect(mockStorage.removeItem).toHaveBeenCalledWith("afl_history_ru");
  });

  it("gracefully handles corrupted storage data", () => {
    mockStorage.store.set("afl_history_ru", "not-valid-json{{{");

    const { result } = renderHook(() => useSearchHistory("ru"));
    expect(result.current.items).toEqual([]);
  });

  it("gracefully handles invalid schema in storage", () => {
    mockStorage.store.set(
      "afl_history_ru",
      JSON.stringify([{ invalid: true }]),
    );

    const { result } = renderHook(() => useSearchHistory("ru"));
    expect(result.current.items).toEqual([]);
  });

  // TZ §4.1.9.5: session-scoped — sessionStorage, NOT localStorage
  it("4.1.9.5-R: writes to sessionStorage, not localStorage", () => {
    const localStorageMock = createMockSessionStorage();
    vi.stubGlobal("localStorage", localStorageMock);

    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
    });

    // sessionStorage.setItem must have been called with the history key
    expect(mockStorage.setItem).toHaveBeenCalledWith(
      "afl_history_ru",
      expect.any(String),
    );
    // localStorage must NOT have been touched for history writes
    expect(localStorageMock.setItem).not.toHaveBeenCalledWith(
      "afl_history_ru",
      expect.any(String),
    );
  });

  // TZ §4.1.9.5: supports board-route, schedule-route, flight-number types
  it("4.1.9.5-R: stores schedule-route items correctly", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(scheduleItem);
    });

    expect(result.current.items[0]).toMatchObject({
      type: "schedule-route",
      params: { departure: "SVO", arrival: "LED" },
    });
  });

  // TZ §4.1.9.5: ordering — most-recent first
  it("4.1.9.5-R: items are ordered most-recent first", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));

    act(() => {
      result.current.add(flightItem);
      result.current.add(routeItem);
      result.current.add(scheduleItem);
    });

    expect(result.current.items[0]?.type).toBe("schedule-route");
    expect(result.current.items[1]?.type).toBe("board-route");
    expect(result.current.items[2]?.type).toBe("flight-number");
  });

  // TZ §4.1.9.5: block shows when there are items, hidden when empty
  it("4.1.9.5-R: returns empty array initially (block starts hidden)", () => {
    const { result } = renderHook(() => useSearchHistory("ru"));
    expect(result.current.items).toHaveLength(0);
  });
});
