import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import Logger from 'src/application/logging/logger';
import { LocalStorageEventListener } from 'src/application/models/localStorageEventListener';
import { LocalStorageKeys, LocalStorageProvider, useLocalStorage } from '../localStorageContext';

// Mock the Logger to avoid console output in tests
vi.spyOn(Logger.prototype, 'logDebug').mockImplementation(() => {});
vi.spyOn(Logger.prototype, 'logWarn').mockImplementation(() => {});
vi.spyOn(Logger.prototype, 'logError').mockImplementation(() => {});

// Component that uses the useLocalStorage hook
const MockLocalStorageComponent: React.FC = () => {
  const {
    setItem,
    getItem,
    setSpiffyOnFeatureFlag,
    getSpiffyOnFeatureFlag,
    isAvailable,
    attachListener,
    detachListener,
  } = useLocalStorage();

  const [testValue, setTestValue] = React.useState<string | null>(null);
  const [spiffyOnValue, setSpiffyOnValue] = React.useState<string | null>(null);
  const [eventReceived, setEventReceived] = React.useState(false);

  React.useEffect(() => {
    // Test getItem
    const value = getItem('test-key');
    setTestValue(value);

    // Test getSpiffyOnFeatureFlag
    const spiffyOn = getSpiffyOnFeatureFlag();
    setSpiffyOnValue(spiffyOn);

    // Test attachListener
    const listener: LocalStorageEventListener = {
      storageKey: 'test-listener-key',
      listener: () => {
        setEventReceived(true);
      },
    };
    attachListener(listener);

    return () => {
      detachListener(listener);
    };
  }, [getItem, getSpiffyOnFeatureFlag, attachListener, detachListener]);

  return (
    <div data-testid="local-storage-component">
      <div data-testid="is-available">{isAvailable.toString()}</div>
      <div data-testid="test-value">{testValue || 'null'}</div>
      <div data-testid="spiffy-on-value">{spiffyOnValue || 'null'}</div>
      <div data-testid="event-received">{eventReceived.toString()}</div>
      <button
        data-testid="set-item-btn"
        type="button"
        onClick={() => {
          setItem('test-key', 'test-value');
          setTestValue(getItem('test-key'));
        }}
      >
        Set Item
      </button>
      <button
        data-testid="set-spiffy-on-true-btn"
        type="button"
        onClick={() => {
          setSpiffyOnFeatureFlag(true);
          setSpiffyOnValue(getSpiffyOnFeatureFlag());
        }}
      >
        Set Spiffy On True
      </button>
      <button
        data-testid="set-spiffy-on-false-btn"
        type="button"
        onClick={() => {
          setSpiffyOnFeatureFlag(false);
          setSpiffyOnValue(getSpiffyOnFeatureFlag());
        }}
      >
        Set Spiffy On False
      </button>
      <button
        data-testid="trigger-event-btn"
        type="button"
        onClick={() => {
          setItem('test-listener-key', 'event-value');
        }}
      >
        Trigger Event
      </button>
    </div>
  );
};

describe('LocalStorageProvider', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Clear localStorage before each test
    if (typeof localStorage !== 'undefined') {
      localStorage.clear();
    }
  });

  describe('useLocalStorage Hook Integration', () => {
    it('should provide localStorage context through useLocalStorage hook', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      // Verify all context methods are accessible
      expect(screen.getByTestId('local-storage-component')).toBeInTheDocument();
    });

    it('should throw error when used outside of LocalStorageProvider', () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

      const TestComponent: React.FC = () => {
        try {
          useLocalStorage();
          return <div data-testid="no-error">No Error</div>;
        } catch (error: any) {
          return <div data-testid="error">{error.message}</div>;
        }
      };

      render(<TestComponent />);

      expect(screen.getByTestId('error')).toHaveTextContent(
        'useLocalStorage must be used within a LocalStorageProvider',
      );

      consoleSpy.mockRestore();
    });
  });

  describe('isAvailable State', () => {
    it('should be true when localStorage is available', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });
    });

    it('should be false when localStorage is not available', async () => {
      const originalLocalStorage = window.localStorage;
      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('false');
      });

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });

    it('should log error when localStorage is not available', async () => {
      const logErrorSpy = vi.spyOn(Logger.prototype, 'logError');
      const originalLocalStorage = window.localStorage;
      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      render(
        <LocalStorageProvider>
          <div>Test</div>
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(logErrorSpy).toHaveBeenCalledWith('localStorage is not available', undefined);
      });

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });
  });

  describe('setItem and getItem', () => {
    it('should set and get items from localStorage', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('set-item-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('test-value')).toHaveTextContent('test-value');
      });

      // Verify it's actually in localStorage
      expect(localStorage.getItem('test-key')).toBe('test-value');
    });

    it("should return null when item doesn't exist", async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('test-value')).toHaveTextContent('null');
      });
    });

    it('should dispatch storage event when setItem is called', async () => {
      const eventHandler = vi.fn();
      window.addEventListener('storage', eventHandler);

      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('set-item-btn').click();
      });

      await waitFor(() => {
        expect(eventHandler).toHaveBeenCalled();
        const event = eventHandler.mock.calls[0][0] as StorageEvent;
        expect(event.key).toBe('test-key');
        expect(event.newValue).toBe('test-value');
      });

      window.removeEventListener('storage', eventHandler);
    });

    it('should not set item when localStorage is not available', async () => {
      const originalLocalStorage = window.localStorage;
      const mockLocalStorage = {
        setItem: vi.fn(),
        getItem: vi.fn(),
        removeItem: vi.fn(),
        clear: vi.fn(),
        key: vi.fn(),
        length: 0,
      };

      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      const TestComponent: React.FC = () => {
        const { setItem } = useLocalStorage();
        React.useEffect(() => {
          setItem('test-key', 'test-value');
        }, [setItem]);
        return <div>Test</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        // setItem should not throw, just return early
        expect(mockLocalStorage.setItem).not.toHaveBeenCalled();
      });

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });

    it('should not get item when localStorage is not available', async () => {
      const originalLocalStorage = window.localStorage;
      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      const TestComponent: React.FC = () => {
        const { getItem } = useLocalStorage();
        const value = getItem('test-key');
        return <div data-testid="value">{value || 'null'}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('value')).toHaveTextContent('null');
      });

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });
  });

  describe('setSpiffyOnFeatureFlag and getSpiffyOnFeatureFlag', () => {
    it('should set SpiffyOn feature flag to true', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('set-spiffy-on-true-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('spiffy-on-value')).toHaveTextContent('true');
      });

      // Verify it's actually in localStorage
      expect(localStorage.getItem(LocalStorageKeys.SpiffyOnOverride)).toBe('true');
    });

    it('should set SpiffyOn feature flag to false', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('set-spiffy-on-false-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('spiffy-on-value')).toHaveTextContent('false');
      });

      // Verify it's actually in localStorage
      expect(localStorage.getItem(LocalStorageKeys.SpiffyOnOverride)).toBe('false');
    });

    it('should not set feature flag when value is null', async () => {
      const TestComponent: React.FC = () => {
        const { setSpiffyOnFeatureFlag, getSpiffyOnFeatureFlag } = useLocalStorage();
        React.useEffect(() => {
          setSpiffyOnFeatureFlag(null);
          const value = getSpiffyOnFeatureFlag();
          (window as any).spiffyOnValue = value;
        }, [setSpiffyOnFeatureFlag, getSpiffyOnFeatureFlag]);
        return <div>Test</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        // When null is passed, it should not set anything
        expect((window as any).spiffyOnValue).toBeNull();
      });
    });

    it('should return null when feature flag is not set', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('spiffy-on-value')).toHaveTextContent('null');
      });
    });

    it('should not set feature flag when localStorage is not available', async () => {
      const originalLocalStorage = window.localStorage;
      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      const TestComponent: React.FC = () => {
        const { setSpiffyOnFeatureFlag, isAvailable } = useLocalStorage();
        React.useEffect(() => {
          setSpiffyOnFeatureFlag(true);
        }, [setSpiffyOnFeatureFlag]);
        return <div data-testid="is-available">{isAvailable.toString()}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        // Should not throw, just return early
        expect(screen.getByTestId('is-available')).toHaveTextContent('false');
      });

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });
  });

  describe('attachListener and detachListener', () => {
    it('should attach and trigger storage event listener', async () => {
      render(
        <LocalStorageProvider>
          <MockLocalStorageComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-available')).toHaveTextContent('true');
      });

      // Wait a bit to ensure listener is attached
      await new Promise(resolve => {
        setTimeout(resolve, 50);
      });

      await act(async () => {
        screen.getByTestId('trigger-event-btn').click();
      });

      await waitFor(
        () => {
          expect(screen.getByTestId('event-received')).toHaveTextContent('true');
        },
        { timeout: 2000 },
      );
    });

    it('should only trigger listener for matching storage key', async () => {
      const listener1 = vi.fn();
      const listener2 = vi.fn();

      const TestComponent: React.FC = () => {
        const { attachListener, setItem, detachListener } = useLocalStorage();
        const [ready, setReady] = React.useState(false);

        React.useEffect(() => {
          const listenerObj1: LocalStorageEventListener = {
            storageKey: 'key1',
            listener: listener1,
          };
          const listenerObj2: LocalStorageEventListener = {
            storageKey: 'key2',
            listener: listener2,
          };
          attachListener(listenerObj1);
          attachListener(listenerObj2);
          setReady(true);

          return () => {
            detachListener(listenerObj1);
            detachListener(listenerObj2);
          };
        }, [attachListener, detachListener]);

        React.useEffect(() => {
          if (ready) {
            // Set item for key1 after listeners are attached
            setTimeout(() => {
              setItem('key1', 'value1');
            }, 50);
          }
        }, [ready, setItem]);

        return <div data-testid="ready">{ready.toString()}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('ready')).toHaveTextContent('true');
      });

      await waitFor(
        () => {
          expect(listener1).toHaveBeenCalled();
          expect(listener2).not.toHaveBeenCalled();
        },
        { timeout: 2000 },
      );
    });

    it('should log debug message when storage event is received', async () => {
      const logDebugSpy = vi.spyOn(Logger.prototype, 'logDebug');

      const TestComponent: React.FC = () => {
        const { attachListener, setItem, detachListener } = useLocalStorage();
        const [ready, setReady] = React.useState(false);

        React.useEffect(() => {
          const listener: LocalStorageEventListener = {
            storageKey: 'test-key',
            listener: () => {},
          };
          attachListener(listener);
          setReady(true);

          return () => {
            detachListener(listener);
          };
        }, [attachListener, detachListener]);

        React.useEffect(() => {
          if (ready) {
            // Set item after listener is attached
            setTimeout(() => {
              setItem('test-key', 'test-value');
            }, 50);
          }
        }, [ready, setItem]);

        return <div data-testid="ready">{ready.toString()}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('ready')).toHaveTextContent('true');
      });

      await waitFor(
        () => {
          expect(logDebugSpy).toHaveBeenCalledWith(
            'Storage event key=test-key, value=',
            'test-value',
          );
        },
        { timeout: 2000 },
      );
    });

    it('should attach and detach listeners without errors', async () => {
      const listener = vi.fn();

      const TestComponent: React.FC = () => {
        const { attachListener, detachListener } = useLocalStorage();
        const [attached, setAttached] = React.useState(false);

        React.useEffect(() => {
          const listenerObj: LocalStorageEventListener = {
            storageKey: 'test-key',
            listener,
          };
          attachListener(listenerObj);
          setAttached(true);

          return () => {
            detachListener(listenerObj);
          };
        }, [attachListener, detachListener]);

        return <div data-testid="attached">{attached.toString()}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('attached')).toHaveTextContent('true');
      });

      // Verify methods can be called without errors
      expect(screen.getByTestId('attached')).toBeInTheDocument();
    });

    it('should not attach listener when localStorage is not available', async () => {
      const originalLocalStorage = window.localStorage;
      const addEventListenerSpy = vi.spyOn(window, 'addEventListener');

      Object.defineProperty(window, 'localStorage', {
        value: undefined,
        writable: true,
        configurable: true,
      });

      const TestComponent: React.FC = () => {
        const { attachListener } = useLocalStorage();
        React.useEffect(() => {
          attachListener({
            storageKey: 'test-key',
            listener: () => {},
          });
        }, [attachListener]);
        return <div>Test</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        // addEventListener should not be called for storage events
        const storageCalls = addEventListenerSpy.mock.calls.filter(
          (call: any) => call[0] === 'storage',
        );
        expect(storageCalls.length).toBe(0);
      });

      addEventListenerSpy.mockRestore();

      // Restore localStorage
      Object.defineProperty(window, 'localStorage', {
        value: originalLocalStorage,
        writable: true,
        configurable: true,
      });
    });

    it('should allow attaching and detaching multiple listeners', async () => {
      const listener1 = vi.fn();
      const listener2 = vi.fn();

      const TestComponent: React.FC = () => {
        const { attachListener, detachListener } = useLocalStorage();
        const [ready, setReady] = React.useState(false);

        React.useEffect(() => {
          const listenerObj1: LocalStorageEventListener = {
            storageKey: 'key1',
            listener: listener1,
          };
          const listenerObj2: LocalStorageEventListener = {
            storageKey: 'key2',
            listener: listener2,
          };

          attachListener(listenerObj1);
          attachListener(listenerObj2);
          // Detach one listener
          detachListener(listenerObj1);
          setReady(true);

          return () => {
            detachListener(listenerObj1);
            detachListener(listenerObj2);
          };
        }, [attachListener, detachListener]);

        return <div data-testid="ready">{ready.toString()}</div>;
      };

      render(
        <LocalStorageProvider>
          <TestComponent />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('ready')).toHaveTextContent('true');
      });

      // Verify methods can be called without errors
      expect(screen.getByTestId('ready')).toBeInTheDocument();
    });
  });

  describe('Context Value Memoization', () => {
    it('should maintain stable context value references', async () => {
      let contextValue1: any;
      let contextValue2: any;

      const Component1: React.FC = () => {
        const context = useLocalStorage();
        contextValue1 = context;
        return <div>Component1</div>;
      };

      const Component2: React.FC = () => {
        const context = useLocalStorage();
        contextValue2 = context;
        return <div>Component2</div>;
      };

      render(
        <LocalStorageProvider>
          <Component1 />
          <Component2 />
        </LocalStorageProvider>,
      );

      await waitFor(() => {
        expect(contextValue1).toBeDefined();
        expect(contextValue2).toBeDefined();
        // Both components should receive the same context instance
        expect(contextValue1).toBe(contextValue2);
      });
    });
  });
});
