import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useAppPreferences, readAppPreferences, type AppPreferences } from "./useAppPreferences.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_prefs";

const DEFAULT: AppPreferences = {
  hideCompletedProjects: true,
  timelineDisplayMode: "strip",
  heatmapColorScheme: "green-red",
  showDemandProjects: true,
  blinkOverbookedDays: false,
};

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

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

beforeEach(() => {
  localStorageMock.clear();
  vi.clearAllMocks();
});

afterEach(() => {
  localStorageMock.clear();
});

// ---------------------------------------------------------------------------
// readAppPreferences (standalone utility)
// ---------------------------------------------------------------------------

describe("readAppPreferences", () => {
  it("returns defaults when localStorage is empty", () => {
    expect(readAppPreferences()).toEqual(DEFAULT);
  });

  it("merges stored partial values with defaults", () => {
    seedStorage({ blinkOverbookedDays: true });
    expect(readAppPreferences()).toEqual({ ...DEFAULT, blinkOverbookedDays: true });
  });

  it("returns defaults when stored JSON is invalid", () => {
    localStorageData[STORAGE_KEY] = "not-valid-json{{{";
    expect(readAppPreferences()).toEqual(DEFAULT);
  });

  it("fills in missing fields from defaults when schema evolves", () => {
    // Simulate a stored object that is missing the newer blinkOverbookedDays key
    const partial = { hideCompletedProjects: false, timelineDisplayMode: "bar" };
    localStorageData[STORAGE_KEY] = JSON.stringify(partial);
    const result = readAppPreferences();
    expect(result.hideCompletedProjects).toBe(false);
    expect(result.timelineDisplayMode).toBe("bar");
    // Fields not in storage must fall back to defaults
    expect(result.blinkOverbookedDays).toBe(DEFAULT.blinkOverbookedDays);
    expect(result.showDemandProjects).toBe(DEFAULT.showDemandProjects);
  });
});

// ---------------------------------------------------------------------------
// useAppPreferences
// ---------------------------------------------------------------------------

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

  it("initialises from stored values", () => {
    seedStorage({ blinkOverbookedDays: true, timelineDisplayMode: "heatmap" });
    const { result } = renderHook(() => useAppPreferences());
    expect(result.current.prefs.blinkOverbookedDays).toBe(true);
    expect(result.current.prefs.timelineDisplayMode).toBe("heatmap");
  });

  // --- setHideCompletedProjects ---

  it("setHideCompletedProjects updates the preference", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setHideCompletedProjects(false);
    });
    expect(result.current.prefs.hideCompletedProjects).toBe(false);
  });

  it("setHideCompletedProjects persists to localStorage", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setHideCompletedProjects(false);
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as AppPreferences;
    expect(stored.hideCompletedProjects).toBe(false);
  });

  it("setHideCompletedProjects back to true restores the default", () => {
    seedStorage({ hideCompletedProjects: false });
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setHideCompletedProjects(true);
    });
    expect(result.current.prefs.hideCompletedProjects).toBe(true);
  });

  // --- setTimelineDisplayMode ---

  it("setTimelineDisplayMode updates to bar", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setTimelineDisplayMode("bar");
    });
    expect(result.current.prefs.timelineDisplayMode).toBe("bar");
  });

  it("setTimelineDisplayMode updates to heatmap", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setTimelineDisplayMode("heatmap");
    });
    expect(result.current.prefs.timelineDisplayMode).toBe("heatmap");
  });

  it("setTimelineDisplayMode updates to strip", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setTimelineDisplayMode("heatmap");
    });
    act(() => {
      result.current.setTimelineDisplayMode("strip");
    });
    expect(result.current.prefs.timelineDisplayMode).toBe("strip");
  });

  it("setTimelineDisplayMode persists to localStorage", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setTimelineDisplayMode("bar");
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as AppPreferences;
    expect(stored.timelineDisplayMode).toBe("bar");
  });

  // --- setHeatmapColorScheme ---

  it("setHeatmapColorScheme updates to blue-orange", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setHeatmapColorScheme("blue-orange");
    });
    expect(result.current.prefs.heatmapColorScheme).toBe("blue-orange");
  });

  it("setHeatmapColorScheme cycles through all schemes", () => {
    const { result } = renderHook(() => useAppPreferences());
    const schemes = ["green-red", "blue-orange", "purple-yellow", "mono"] as const;
    for (const scheme of schemes) {
      act(() => {
        result.current.setHeatmapColorScheme(scheme);
      });
      expect(result.current.prefs.heatmapColorScheme).toBe(scheme);
    }
  });

  it("setHeatmapColorScheme persists to localStorage", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setHeatmapColorScheme("mono");
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as AppPreferences;
    expect(stored.heatmapColorScheme).toBe("mono");
  });

  // --- setShowDemandProjects ---

  it("setShowDemandProjects toggles to false", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setShowDemandProjects(false);
    });
    expect(result.current.prefs.showDemandProjects).toBe(false);
  });

  it("setShowDemandProjects persists to localStorage", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setShowDemandProjects(false);
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as AppPreferences;
    expect(stored.showDemandProjects).toBe(false);
  });

  // --- setBlinkOverbookedDays ---

  it("setBlinkOverbookedDays toggles to true", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setBlinkOverbookedDays(true);
    });
    expect(result.current.prefs.blinkOverbookedDays).toBe(true);
  });

  it("setBlinkOverbookedDays persists to localStorage", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setBlinkOverbookedDays(true);
    });
    const stored = JSON.parse(localStorageData[STORAGE_KEY] ?? "{}") as AppPreferences;
    expect(stored.blinkOverbookedDays).toBe(true);
  });

  // --- isolated updates do not overwrite other prefs ---

  it("updating one pref does not clobber unrelated prefs", () => {
    const { result } = renderHook(() => useAppPreferences());
    act(() => {
      result.current.setTimelineDisplayMode("bar");
    });
    act(() => {
      result.current.setBlinkOverbookedDays(true);
    });
    expect(result.current.prefs.timelineDisplayMode).toBe("bar");
    expect(result.current.prefs.blinkOverbookedDays).toBe(true);
    // All other fields stay at defaults
    expect(result.current.prefs.hideCompletedProjects).toBe(DEFAULT.hideCompletedProjects);
    expect(result.current.prefs.showDemandProjects).toBe(DEFAULT.showDemandProjects);
    expect(result.current.prefs.heatmapColorScheme).toBe(DEFAULT.heatmapColorScheme);
  });

  // --- cross-instance sync via CustomEvent ---

  it("dispatching the change event syncs a second hook instance", () => {
    const { result: r1 } = renderHook(() => useAppPreferences());
    const { result: r2 } = renderHook(() => useAppPreferences());

    act(() => {
      r1.current.setBlinkOverbookedDays(true);
    });

    // r2 should receive the updated prefs via the CustomEvent listener
    expect(r2.current.prefs.blinkOverbookedDays).toBe(true);
  });

  // --- stable setter references ---

  it("setter functions are stable across re-renders", () => {
    const { result, rerender } = renderHook(() => useAppPreferences());
    const setters = {
      hide: result.current.setHideCompletedProjects,
      mode: result.current.setTimelineDisplayMode,
      scheme: result.current.setHeatmapColorScheme,
      demand: result.current.setShowDemandProjects,
      blink: result.current.setBlinkOverbookedDays,
    };
    rerender();
    expect(result.current.setHideCompletedProjects).toBe(setters.hide);
    expect(result.current.setTimelineDisplayMode).toBe(setters.mode);
    expect(result.current.setHeatmapColorScheme).toBe(setters.scheme);
    expect(result.current.setShowDemandProjects).toBe(setters.demand);
    expect(result.current.setBlinkOverbookedDays).toBe(setters.blink);
  });
});
