import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
  loadPref,
  savePref,
  applyTheme,
  STORAGE_PREFIX,
  THEMES,
} from "../../src/components/settings/helpers";
import type { ThemeName } from "../../src/components/settings/helpers";

describe("settings/helpers", () => {
  let container: HTMLDivElement;

  beforeEach(() => {
    container = document.createElement("div");
    document.body.appendChild(container);
    localStorage.clear();
  });

  afterEach(() => {
    container.remove();
    localStorage.clear();
  });

  describe("STORAGE_PREFIX", () => {
    it("has the correct prefix value", () => {
      expect(STORAGE_PREFIX).toBe("owncord:settings:");
    });
  });

  describe("loadPref", () => {
    it("returns fallback when key does not exist", () => {
      const result = loadPref("nonexistent", "default");
      expect(result).toBe("default");
    });

    it("returns stored value when key exists", () => {
      localStorage.setItem(STORAGE_PREFIX + "theme", JSON.stringify("midnight"));
      const result = loadPref("theme", "dark");
      expect(result).toBe("midnight");
    });

    it("returns fallback on invalid JSON", () => {
      localStorage.setItem(STORAGE_PREFIX + "broken", "not-valid-json");
      const result = loadPref("broken", "fallback");
      expect(result).toBe("fallback");
    });

    it("handles boolean values", () => {
      localStorage.setItem(STORAGE_PREFIX + "notifications", JSON.stringify(true));
      expect(loadPref("notifications", false)).toBe(true);
    });

    it("handles numeric values", () => {
      localStorage.setItem(STORAGE_PREFIX + "volume", JSON.stringify(75));
      expect(loadPref("volume", 50)).toBe(75);
    });

    it("handles object values", () => {
      const obj = { fontSize: 14, compact: true };
      localStorage.setItem(STORAGE_PREFIX + "display", JSON.stringify(obj));
      const result = loadPref("display", {});
      expect(result).toEqual(obj);
    });
  });

  describe("savePref", () => {
    it("stores value with correct prefix", () => {
      savePref("theme", "midnight");
      const raw = localStorage.getItem(STORAGE_PREFIX + "theme");
      expect(raw).toBe(JSON.stringify("midnight"));
    });

    it("stores boolean values", () => {
      savePref("notifications", true);
      const raw = localStorage.getItem(STORAGE_PREFIX + "notifications");
      expect(raw).toBe("true");
    });

    it("stores numeric values", () => {
      savePref("volume", 80);
      const raw = localStorage.getItem(STORAGE_PREFIX + "volume");
      expect(raw).toBe("80");
    });

    it("stores object values", () => {
      const obj = { a: 1, b: "two" };
      savePref("config", obj);
      const raw = localStorage.getItem(STORAGE_PREFIX + "config");
      expect(JSON.parse(raw!)).toEqual(obj);
    });

    it("overwrites existing values", () => {
      savePref("theme", "dark");
      savePref("theme", "light");
      expect(loadPref("theme", "dark")).toBe("light");
    });
  });

  describe("applyTheme", () => {
    it("sets CSS custom properties for dark theme", () => {
      applyTheme("dark");
      const root = document.documentElement;
      expect(root.style.getPropertyValue("--bg-primary")).toBe("#313338");
      expect(root.style.getPropertyValue("--bg-secondary")).toBe("#2b2d31");
      expect(root.style.getPropertyValue("--bg-tertiary")).toBe("#1e1f22");
      expect(root.style.getPropertyValue("--text-normal")).toBe("#dbdee1");
    });

    it("sets CSS custom properties for midnight theme", () => {
      applyTheme("midnight");
      const root = document.documentElement;
      expect(root.style.getPropertyValue("--bg-primary")).toBe("#1a1a2e");
    });

    it("sets CSS custom properties for light theme", () => {
      applyTheme("light");
      const root = document.documentElement;
      expect(root.style.getPropertyValue("--bg-primary")).toBe("#ffffff");
      expect(root.style.getPropertyValue("--text-normal")).toBe("#313338");
    });

    it("overwrites previous theme variables", () => {
      applyTheme("dark");
      applyTheme("light");
      const root = document.documentElement;
      expect(root.style.getPropertyValue("--bg-primary")).toBe("#ffffff");
    });

    it("switching away from light clears the light-only tokens instead of leaving them stuck on <html>", () => {
      // OC-0201: applyTheme only ever *sets* the keys present in the new
      // theme and never clears keys the previous theme set. THEMES.light
      // defines ~24 custom properties while dark/midnight/neon-glow define
      // only 4, so switching light -> dark must leave zero light-only
      // tokens behind on document.documentElement.
      applyTheme("light");
      const root = document.documentElement;
      expect(root.style.getPropertyValue("--bg-input")).toBe("#ebedef");

      applyTheme("dark");

      // The 4 keys dark actually owns must reflect dark's values.
      expect(root.style.getPropertyValue("--bg-primary")).toBe("#313338");
      expect(root.style.getPropertyValue("--text-normal")).toBe("#dbdee1");

      // Every light-only token must be cleared, not left stuck at its
      // light-mode value (which would outrank the :root CSS default via
      // inline-style specificity).
      const lightOnlyKeys = Object.keys(THEMES.light).filter((k) => !(k in THEMES.dark));
      expect(lightOnlyKeys.length).toBeGreaterThan(0);
      for (const key of lightOnlyKeys) {
        expect(
          root.style.getPropertyValue(key),
          `${key} must be cleared after switching to dark`,
        ).toBe("");
      }
    });

    it("does not clear unrelated inline custom properties like --accent or --font-size", () => {
      const root = document.documentElement;
      root.style.setProperty("--accent", "#00c8ff");
      root.style.setProperty("--font-size", "18px");

      applyTheme("light");
      applyTheme("dark");

      expect(root.style.getPropertyValue("--accent")).toBe("#00c8ff");
      expect(root.style.getPropertyValue("--font-size")).toBe("18px");
    });

    it("light theme overrides the dark-mode input/border/interactive tokens so composer and form fields aren't dark-on-dark", () => {
      // OC-0043: the light theme only overrode 4 of ~45 tokens. --bg-input
      // (used by .message-input-box, .msg-textarea, .form-input, .reply-bar-inner)
      // was never overridden, so it kept tokens.css's dark default while
      // --text-normal flipped to dark text -- unreadable dark-on-dark.
      applyTheme("light");
      const root = document.documentElement;
      const darkDefaults: Record<string, string> = {
        "--bg-input": "#383a40",
        "--bg-hover": "#35373c",
        "--bg-active": "#404249",
        "--border": "#3f4147",
        "--border-strong": "#4e5058",
        "--text-muted": "#949ba4",
        "--text-faint": "#80848e",
        "--text-micro": "#6d6f78",
        "--header-primary": "#f2f3f5",
        "--header-secondary": "#b5bac1",
        "--interactive-normal": "#b5bac1",
        "--interactive-hover": "#dbdee1",
        "--interactive-active": "#fff",
        "--interactive-muted": "#4e5058",
      };
      for (const [token, darkValue] of Object.entries(darkDefaults)) {
        const applied = root.style.getPropertyValue(token);
        expect(applied, `${token} must be set by the light theme`).not.toBe("");
        expect(applied, `${token} must not keep its dark-mode value`).not.toBe(darkValue);
      }
    });
  });

  describe("THEMES", () => {
    it("contains dark, midnight, and light themes", () => {
      const themeNames = Object.keys(THEMES);
      expect(themeNames).toContain("dark");
      expect(themeNames).toContain("midnight");
      expect(themeNames).toContain("light");
    });

    it("each theme has required CSS variables", () => {
      for (const [, vars] of Object.entries(THEMES)) {
        expect(vars).toHaveProperty("--bg-primary");
        expect(vars).toHaveProperty("--bg-secondary");
        expect(vars).toHaveProperty("--bg-tertiary");
        expect(vars).toHaveProperty("--text-normal");
      }
    });
  });
});
