import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useTheme, type ThemePreferences } from "./useTheme.js";

// ---------------------------------------------------------------------------
// localStorage stub
// ---------------------------------------------------------------------------

const localStorageData: Record<string, string> = {};

const localStorageMock = {
  getItem: vi.fn((key: string) => localStorageData[key] ?? null),
  setItem: vi.fn((key: string, value: string) => {
    localStorageData[key] = value;
  }),
  removeItem: vi.fn((key: string) => {
    delete localStorageData[key];
  }),
  clear: vi.fn(() => {
    for (const k in localStorageData) delete localStorageData[k];
  }),
  length: 0,
  key: vi.fn(),
};

Object.defineProperty(globalThis, "localStorage", {
  value: localStorageMock,
  writable: true,
});

const STORAGE_KEY = "nexus_theme";

const DEFAULT: ThemePreferences = { mode: "light", accent: "sky" };

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

function seedStorage(partial: Partial<ThemePreferences>) {
  localStorageData[STORAGE_KEY] = JSON.stringify(partial);
}

beforeEach(() => {
  localStorageMock.clear();
  vi.clearAllMocks();
  // Reset document.documentElement state between tests
  document.documentElement.classList.remove("dark");
  document.documentElement.removeAttribute("data-accent");
});

afterEach(() => {
  localStorageMock.clear();
  document.documentElement.classList.remove("dark");
  document.documentElement.removeAttribute("data-accent");
});

// ---------------------------------------------------------------------------
// useTheme
// ---------------------------------------------------------------------------

describe("useTheme", () => {
  // --- initial state ---

  it("initialises with default prefs when storage is empty", () => {
    const { result } = renderHook(() => useTheme());
    expect(result.current.prefs).toEqual(DEFAULT);
  });

  it("initialises from stored prefs", () => {
    seedStorage({ mode: "dark", accent: "violet" });
    const { result } = renderHook(() => useTheme());
    expect(result.current.prefs.mode).toBe("dark");
    expect(result.current.prefs.accent).toBe("violet");
  });

  // --- DOM side-effects on mount ---

  it("applies light mode on mount (no 'dark' class)", () => {
    renderHook(() => useTheme());
    expect(document.documentElement.classList.contains("dark")).toBe(false);
  });

  it("adds 'dark' class when stored mode is dark", () => {
    seedStorage({ mode: "dark", accent: "sky" });
    renderHook(() => useTheme());
    expect(document.documentElement.classList.contains("dark")).toBe(true);
  });

  it("sets data-accent attribute on mount", () => {
    renderHook(() => useTheme());
    expect(document.documentElement.getAttribute("data-accent")).toBe("sky");
  });

  it("sets data-accent from stored value on mount", () => {
    seedStorage({ mode: "light", accent: "rose" });
    renderHook(() => useTheme());
    expect(document.documentElement.getAttribute("data-accent")).toBe("rose");
  });

  // --- setMode ---

  it("setMode('dark') updates prefs.mode", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    expect(result.current.prefs.mode).toBe("dark");
  });

  it("setMode('dark') adds the 'dark' class to <html>", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    expect(document.documentElement.classList.contains("dark")).toBe(true);
  });

  it("setMode('light') removes the 'dark' class from <html>", () => {
    seedStorage({ mode: "dark", accent: "sky" });
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("light");
    });
    expect(document.documentElement.classList.contains("dark")).toBe(false);
  });

  it("setMode persists to localStorage", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as ThemePreferences;
    expect(stored.mode).toBe("dark");
  });

  it("setMode does not overwrite accent", () => {
    seedStorage({ mode: "light", accent: "indigo" });
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    expect(result.current.prefs.accent).toBe("indigo");
  });

  // --- setAccent ---

  it("setAccent updates prefs.accent", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setAccent("emerald");
    });
    expect(result.current.prefs.accent).toBe("emerald");
  });

  it("setAccent updates data-accent on <html>", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setAccent("amber");
    });
    expect(document.documentElement.getAttribute("data-accent")).toBe("amber");
  });

  it("setAccent persists to localStorage", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setAccent("violet");
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as ThemePreferences;
    expect(stored.accent).toBe("violet");
  });

  it("setAccent does not overwrite mode", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    act(() => {
      result.current.setAccent("rose");
    });
    expect(result.current.prefs.mode).toBe("dark");
  });

  it("setAccent cycles through all accent values", () => {
    const { result } = renderHook(() => useTheme());
    const accents = ["sky", "indigo", "violet", "emerald", "rose", "amber"] as const;
    for (const accent of accents) {
      act(() => {
        result.current.setAccent(accent);
      });
      expect(result.current.prefs.accent).toBe(accent);
      expect(document.documentElement.getAttribute("data-accent")).toBe(accent);
    }
  });

  // --- combined mode + accent change ---

  it("changing both mode and accent reflects correct DOM state", () => {
    const { result } = renderHook(() => useTheme());
    act(() => {
      result.current.setMode("dark");
    });
    act(() => {
      result.current.setAccent("indigo");
    });
    expect(document.documentElement.classList.contains("dark")).toBe(true);
    expect(document.documentElement.getAttribute("data-accent")).toBe("indigo");
  });

  // --- stable setter references ---

  it("setter functions are stable across re-renders", () => {
    const { result, rerender } = renderHook(() => useTheme());
    const setMode = result.current.setMode;
    const setAccent = result.current.setAccent;
    rerender();
    expect(result.current.setMode).toBe(setMode);
    expect(result.current.setAccent).toBe(setAccent);
  });
});
