/* @vitest-environment node */

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
	SEARCH_SCOPE,
	DEFAULT_SEARCH_LIMIT,
} from "@/lib/frontend/search/urlState";
import {
	buildSearchHistoryStorageKey,
	loadSearchHistory,
	saveSearchHistory,
	addSearchHistoryEntry,
	clearSearchHistory,
	normalizeSearchHistoryEntry,
	buildSearchHrefFromEntry,
} from "./history.js";

function createLocalStorageMock() {
	const store = new Map();

	return {
		getItem: vi.fn((key) => (store.has(key) ? store.get(key) : null)),
		setItem: vi.fn((key, value) => {
			store.set(String(key), String(value));
		}),
		removeItem: vi.fn((key) => {
			store.delete(String(key));
		}),
		clear: vi.fn(() => {
			store.clear();
		}),
		dump: () => store,
	};
}

describe("lib/frontend/search/history", () => {
	let localStorageMock;

	beforeEach(() => {
		localStorageMock = createLocalStorageMock();
		vi.stubGlobal("window", { localStorage: localStorageMock });
	});

	afterEach(() => {
		vi.unstubAllGlobals();
		vi.restoreAllMocks();
	});

	it("buildSearchHistoryStorageKey is user-scoped and versioned", () => {
		expect(buildSearchHistoryStorageKey("u-1")).toBe("rhl.searchHistory.v1.u-1");
		expect(buildSearchHistoryStorageKey("  ")).toBe(null);
		expect(buildSearchHistoryStorageKey(null)).toBe(null);
	});

	it("normalizeSearchHistoryEntry canonicalizes values", () => {
		const normalized = normalizeSearchHistoryEntry({
			routeBranch: " nl1 ",
			q: "  bridgestone  ",
			scope: SEARCH_SCOPE.MULTI,
			branches: ["nl20", "NL06", "NL20", "bad"],
			limit: 999,
			from: " 2025-01-01 ",
			to: " ",
			createdAt: "1700000000000",
		});

		expect(normalized).toEqual({
			routeBranch: "NL1",
			q: "bridgestone",
			scope: SEARCH_SCOPE.MULTI,
			branches: ["NL06", "NL20"],
			limit: DEFAULT_SEARCH_LIMIT,
			from: "2025-01-01",
			to: null,
			createdAt: 1700000000000,
		});
	});

	it("save/load keeps deterministic normalized entries and dedupes by canonical identity", () => {
		const userId = "admin-1";

		const saved = saveSearchHistory(userId, [
			{
				routeBranch: "NL17",
				q: "abc",
				scope: SEARCH_SCOPE.MULTI,
				branches: ["NL20", "NL06"],
				limit: 200,
				from: "2025-01-01",
				to: "2025-01-31",
				createdAt: 100,
			},
			{
				routeBranch: "NL17",
				q: " abc ",
				scope: SEARCH_SCOPE.MULTI,
				branches: ["NL06", "NL20", "NL20"],
				limit: 200,
				from: "2025-01-01",
				to: "2025-01-31",
				createdAt: 200,
			},
		]);

		expect(saved).toHaveLength(1);
		expect(saved[0]).toMatchObject({
			routeBranch: "NL17",
			q: "abc",
			scope: SEARCH_SCOPE.MULTI,
			branches: ["NL06", "NL20"],
			limit: 200,
			from: "2025-01-01",
			to: "2025-01-31",
		});

		expect(loadSearchHistory(userId)).toEqual(saved);
	});

	it("addSearchHistoryEntry moves reused queries to the top (LRU)", () => {
		const userId = "dev-1";

		addSearchHistoryEntry(userId, {
			routeBranch: "NL01",
			q: "first",
			scope: SEARCH_SCOPE.SINGLE,
			branches: [],
			limit: 100,
			from: null,
			to: null,
			createdAt: 1,
		});

		addSearchHistoryEntry(userId, {
			routeBranch: "NL02",
			q: "second",
			scope: SEARCH_SCOPE.SINGLE,
			branches: [],
			limit: 100,
			from: null,
			to: null,
			createdAt: 2,
		});

		const next = addSearchHistoryEntry(userId, {
			routeBranch: "NL01",
			q: "first",
			scope: SEARCH_SCOPE.SINGLE,
			branches: [],
			limit: 100,
			from: null,
			to: null,
			createdAt: 3,
		});

		expect(next).toHaveLength(2);
		expect(next[0].routeBranch).toBe("NL01");
		expect(next[0].q).toBe("first");
		expect(next[1].routeBranch).toBe("NL02");
		expect(next[1].q).toBe("second");
	});

	it("addSearchHistoryEntry enforces cap", () => {
		const userId = "cap-user";

		for (let i = 1; i <= 12; i += 1) {
			addSearchHistoryEntry(
				userId,
				{
					routeBranch: "NL17",
					q: `q-${i}`,
					scope: SEARCH_SCOPE.SINGLE,
					branches: [],
					limit: 100,
					from: null,
					to: null,
					createdAt: i,
				},
				{ maxItems: 10 },
			);
		}

		const entries = loadSearchHistory(userId);
		expect(entries).toHaveLength(10);
		expect(entries[0].q).toBe("q-12");
		expect(entries[9].q).toBe("q-3");
	});

	it("clearSearchHistory removes persisted entries", () => {
		const userId = "clear-user";

		saveSearchHistory(userId, [
			{
				routeBranch: "NL17",
				q: "x",
				scope: SEARCH_SCOPE.SINGLE,
				branches: [],
				limit: 100,
				from: null,
				to: null,
				createdAt: 1,
			},
		]);

		expect(loadSearchHistory(userId)).toHaveLength(1);
		expect(clearSearchHistory(userId)).toBe(true);
		expect(loadSearchHistory(userId)).toEqual([]);
	});

	it("buildSearchHrefFromEntry returns only safe internal hrefs", () => {
		const href = buildSearchHrefFromEntry({
			routeBranch: "NL17",
			q: "bridgestone",
			scope: SEARCH_SCOPE.MULTI,
			branches: ["NL20", "NL06"],
			limit: 200,
			from: "2025-01-01",
			to: "2025-01-31",
			createdAt: 1,
		});

		expect(href).toBe(
			"/NL17/search?q=bridgestone&scope=multi&branches=NL06%2CNL20&limit=200&from=2025-01-01&to=2025-01-31",
		);
		expect(href.startsWith("/")).toBe(true);
		expect(href.startsWith("//")).toBe(false);

		expect(
			buildSearchHrefFromEntry({
				routeBranch: "http://evil.com",
				q: "x",
				scope: SEARCH_SCOPE.SINGLE,
				branches: [],
				limit: 100,
				from: null,
				to: null,
				createdAt: 1,
			}),
		).toBe(null);
	});
});

