/**
 * @file useChatOverlay.test.ts
 * @description Tests for the useChatOverlay hook that manages chat overlay state
 */

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

// Mock localStorage
const localStorageMock = ((): Storage => {
  let store: Record<string, string> = {};

  return {
    getItem: (key: string): string | null => store[key] ?? null,
    setItem: (key: string, value: string): void => {
      store[key] = value;
    },
    removeItem: (key: string): void => {
      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
      delete store[key];
    },
    clear: (): void => {
      store = {};
    },
    get length(): number {
      return Object.keys(store).length;
    },
    key: (index: number): string | null => {
      const keys = Object.keys(store);
      return keys[index] ?? null;
    },
  };
})();

Object.defineProperty(window, "localStorage", {
  value: localStorageMock,
});

describe("useChatOverlay", () => {
  beforeEach(() => {
    localStorageMock.clear();
    vi.clearAllMocks();
  });

  describe("initial state", () => {
    it("should initialize with closed and not minimized state", () => {
      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should restore state from localStorage if available", () => {
      localStorageMock.setItem(
        "chatOverlayState",
        JSON.stringify({ isOpen: true, isMinimized: true })
      );

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(true);
      expect(result.current.isMinimized).toBe(true);
    });

    it("should handle invalid localStorage data gracefully", () => {
      vi.spyOn(console, "warn").mockImplementation(() => undefined);
      localStorageMock.setItem("chatOverlayState", "invalid json");

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should fall back to defaults when localStorage has wrong shape", () => {
      vi.spyOn(console, "warn").mockImplementation(() => undefined);
      // Valid JSON but wrong shape
      localStorageMock.setItem("chatOverlayState", JSON.stringify({ isOpen: "yes", wrong: true }));

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should fall back to defaults when localStorage has null value parsed", () => {
      vi.spyOn(console, "warn").mockImplementation(() => undefined);
      localStorageMock.setItem("chatOverlayState", "null");

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should fall back to defaults when localStorage has array instead of object", () => {
      vi.spyOn(console, "warn").mockImplementation(() => undefined);
      localStorageMock.setItem("chatOverlayState", JSON.stringify([true, false]));

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });
  });

  describe("open", () => {
    it("should open the chat overlay", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      expect(result.current.isOpen).toBe(true);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should persist state to localStorage when opening", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as {
        isOpen: boolean;
        isMinimized: boolean;
      };
      expect(stored.isOpen).toBe(true);
      expect(stored.isMinimized).toBe(false);
    });
  });

  describe("close", () => {
    it("should close the chat overlay", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      act(() => {
        result.current.close();
      });

      expect(result.current.isOpen).toBe(false);
    });

    it("should persist state to localStorage when closing", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      act(() => {
        result.current.close();
      });

      const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as {
        isOpen: boolean;
        isMinimized: boolean;
      };
      expect(stored.isOpen).toBe(false);
    });
  });

  describe("minimize", () => {
    it("should minimize the chat overlay", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      act(() => {
        result.current.minimize();
      });

      expect(result.current.isOpen).toBe(true);
      expect(result.current.isMinimized).toBe(true);
    });

    it("should persist minimized state to localStorage", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      act(() => {
        result.current.minimize();
      });

      const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as {
        isOpen: boolean;
        isMinimized: boolean;
      };
      expect(stored.isMinimized).toBe(true);
    });
  });

  describe("expand", () => {
    it("should expand the minimized chat overlay", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
        result.current.minimize();
      });

      act(() => {
        result.current.expand();
      });

      expect(result.current.isMinimized).toBe(false);
    });

    it("should persist expanded state to localStorage", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
        result.current.minimize();
      });

      act(() => {
        result.current.expand();
      });

      const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as {
        isOpen: boolean;
        isMinimized: boolean;
      };
      expect(stored.isMinimized).toBe(false);
    });
  });

  describe("toggle", () => {
    it("should toggle the chat overlay open state", () => {
      const { result } = renderHook(() => useChatOverlay());

      // Initially closed
      expect(result.current.isOpen).toBe(false);

      // Toggle to open
      act(() => {
        result.current.toggle();
      });

      expect(result.current.isOpen).toBe(true);

      // Toggle to close
      act(() => {
        result.current.toggle();
      });

      expect(result.current.isOpen).toBe(false);
    });

    it("should reset minimized state when toggling closed", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
        result.current.minimize();
      });

      expect(result.current.isMinimized).toBe(true);

      act(() => {
        result.current.toggle();
      });

      // Should close and reset minimized (invariant: cannot be minimized when closed)
      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should preserve minimized state when toggling open", () => {
      const { result } = renderHook(() => useChatOverlay());

      // Start closed
      expect(result.current.isOpen).toBe(false);

      act(() => {
        result.current.toggle();
      });

      // Should open and not be minimized
      expect(result.current.isOpen).toBe(true);
      expect(result.current.isMinimized).toBe(false);
    });
  });

  describe("toggleMinimize", () => {
    it("should toggle the minimize state", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
      });

      // Initially not minimized
      expect(result.current.isMinimized).toBe(false);

      // Toggle to minimized
      act(() => {
        result.current.toggleMinimize();
      });

      expect(result.current.isMinimized).toBe(true);

      // Toggle back to expanded
      act(() => {
        result.current.toggleMinimize();
      });

      expect(result.current.isMinimized).toBe(false);
    });
  });

  describe("state invariant enforcement", () => {
    it("should reject invalid localStorage state: closed AND minimized", () => {
      vi.spyOn(console, "warn").mockImplementation(() => undefined);
      // This violates the invariant: cannot be minimized when closed
      localStorageMock.setItem(
        "chatOverlayState",
        JSON.stringify({ isOpen: false, isMinimized: true })
      );

      const { result } = renderHook(() => useChatOverlay());

      // Should fall back to defaults since invariant is violated
      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should accept valid state: open and minimized", () => {
      localStorageMock.setItem(
        "chatOverlayState",
        JSON.stringify({ isOpen: true, isMinimized: true })
      );

      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(true);
      expect(result.current.isMinimized).toBe(true);
    });

    it("should reset isMinimized when closing via close()", () => {
      const { result } = renderHook(() => useChatOverlay());

      act(() => {
        result.current.open();
        result.current.minimize();
      });

      expect(result.current.isMinimized).toBe(true);

      act(() => {
        result.current.close();
      });

      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });
  });

  describe("minimize/toggleMinimize guards", () => {
    it("should not allow minimize when overlay is closed", () => {
      const { result } = renderHook(() => useChatOverlay());

      // Overlay starts closed
      expect(result.current.isOpen).toBe(false);

      act(() => {
        result.current.minimize();
      });

      // Should remain unchanged - cannot minimize when closed
      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });

    it("should not allow toggleMinimize when overlay is closed", () => {
      const { result } = renderHook(() => useChatOverlay());

      expect(result.current.isOpen).toBe(false);

      act(() => {
        result.current.toggleMinimize();
      });

      // Should remain unchanged - cannot toggle minimize when closed
      expect(result.current.isOpen).toBe(false);
      expect(result.current.isMinimized).toBe(false);
    });
  });

  describe("storage error handling", () => {
    it("should call onStorageError when localStorage save fails", () => {
      const onStorageError = vi.fn();
      const quotaError = new DOMException("Storage full", "QuotaExceededError");

      const { result } = renderHook(() => useChatOverlay({ onStorageError }));

      // Make setItem throw
      vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
        throw quotaError;
      });
      vi.spyOn(console, "warn").mockImplementation(() => undefined);

      act(() => {
        result.current.open();
      });

      expect(onStorageError).toHaveBeenCalledWith(
        "Storage is full. Chat state may not persist across page refreshes."
      );
    });

    it("should show appropriate message for SecurityError", () => {
      const onStorageError = vi.fn();
      const securityError = new DOMException("Blocked", "SecurityError");

      const { result } = renderHook(() => useChatOverlay({ onStorageError }));

      vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
        throw securityError;
      });
      vi.spyOn(console, "warn").mockImplementation(() => undefined);

      act(() => {
        result.current.open();
      });

      expect(onStorageError).toHaveBeenCalledWith(
        "Storage is unavailable in this browser mode. Chat state will not persist."
      );
    });

    it("should only notify once per error type", () => {
      const onStorageError = vi.fn();
      const quotaError = new DOMException("Storage full", "QuotaExceededError");

      // Set up spy BEFORE rendering so all saves (including initial) throw
      vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
        throw quotaError;
      });
      vi.spyOn(console, "warn").mockImplementation(() => undefined);

      const { result } = renderHook(() => useChatOverlay({ onStorageError }));

      // Initial render triggers a save which throws → first notification
      // Multiple state changes that trigger more saves
      act(() => {
        result.current.open();
      });
      act(() => {
        result.current.minimize();
      });
      act(() => {
        result.current.expand();
      });

      // Should only have been called once for QuotaExceededError despite multiple failures
      expect(onStorageError).toHaveBeenCalledTimes(1);
    });
  });
});
