// @vitest-environment jsdom
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { getLocationState, useLocation } from './useLocation';

describe('getLocationState', () => {
  let originalLocation: Location;
  let originalHistory: History;

  beforeEach(() => {
    // Save original window properties
    originalLocation = window.location;
    originalHistory = window.history;

    // Set up a basic window location and history
    Object.defineProperty(window, 'location', {
      value: {
        hash: '#test',
        host: 'localhost:3000',
        hostname: 'localhost',
        href: 'http://localhost:3000/path?query=1#test',
        origin: 'http://localhost:3000',
        pathname: '/path',
        port: '3000',
        protocol: 'http:',
        search: '?query=1',
      },
      writable: true,
      configurable: true,
    });

    Object.defineProperty(window, 'history', {
      value: {
        state: { foo: 'bar' },
        length: 5,
      },
      writable: true,
      configurable: true,
    });
  });

  afterEach(() => {
    // Restore original window properties
    Object.defineProperty(window, 'location', {
      value: originalLocation,
      writable: true,
      configurable: true,
    });

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

  it('should return location state with trigger', () => {
    const result = getLocationState('test-trigger');

    expect(result).toEqual({
      trigger: 'test-trigger',
      state: { foo: 'bar' },
      length: 5,
      hash: '#test',
      host: 'localhost:3000',
      hostname: 'localhost',
      href: 'http://localhost:3000/path?query=1#test',
      origin: 'http://localhost:3000',
      pathname: '/path',
      port: '3000',
      protocol: 'http:',
      search: '?query=1',
    });
  });

  it('should handle different triggers', () => {
    const loadResult = getLocationState('load');
    const popstateResult = getLocationState('popstate');
    const hashchangeResult = getLocationState('hashchange');

    expect(loadResult.trigger).toBe('load');
    expect(popstateResult.trigger).toBe('popstate');
    expect(hashchangeResult.trigger).toBe('hashchange');
  });

  it('should include history state', () => {
    const result = getLocationState('test');

    expect(result.state).toEqual({ foo: 'bar' });
    expect(result.length).toBe(5);
  });

  it('should include all location properties', () => {
    const result = getLocationState('test');

    expect(result.hash).toBe('#test');
    expect(result.host).toBe('localhost:3000');
    expect(result.hostname).toBe('localhost');
    expect(result.href).toBe('http://localhost:3000/path?query=1#test');
    expect(result.origin).toBe('http://localhost:3000');
    expect(result.pathname).toBe('/path');
    expect(result.port).toBe('3000');
    expect(result.protocol).toBe('http:');
    expect(result.search).toBe('?query=1');
  });

  it('should handle empty hash', () => {
    Object.defineProperty(window, 'location', {
      value: {
        ...window.location,
        hash: '',
      },
      writable: true,
      configurable: true,
    });

    const result = getLocationState('test');

    expect(result.hash).toBe('');
  });

  it('should handle empty search params', () => {
    Object.defineProperty(window, 'location', {
      value: {
        ...window.location,
        search: '',
      },
      writable: true,
      configurable: true,
    });

    const result = getLocationState('test');

    expect(result.search).toBe('');
  });

  it('should handle null history state', () => {
    Object.defineProperty(window, 'history', {
      value: {
        state: null,
        length: 1,
      },
      writable: true,
      configurable: true,
    });

    const result = getLocationState('test');

    expect(result.state).toBeNull();
  });
});

describe('useLocation', () => {
  let eventListeners: Record<string, EventListener[]>;
  let originalLocation: Location;
  let originalHistory: History;

  beforeEach(() => {
    // Save original window properties
    originalLocation = window.location;
    originalHistory = window.history;

    eventListeners = {};

    // Mock addEventListener and removeEventListener
    vi.spyOn(window, 'addEventListener').mockImplementation(
      (event: string, handler: EventListener) => {
        if (!eventListeners[event]) {
          eventListeners[event] = [];
        }
        eventListeners[event].push(handler);
      }
    );

    vi.spyOn(window, 'removeEventListener').mockImplementation(
      (event: string, handler: EventListener) => {
        if (eventListeners[event]) {
          eventListeners[event] = eventListeners[event].filter(
            (h) => h !== handler
          );
        }
      }
    );

    // Set up initial location
    Object.defineProperty(window, 'location', {
      value: {
        hash: '',
        host: 'localhost:3000',
        hostname: 'localhost',
        href: 'http://localhost:3000/',
        origin: 'http://localhost:3000',
        pathname: '/',
        port: '3000',
        protocol: 'http:',
        search: '',
      },
      writable: true,
      configurable: true,
    });

    Object.defineProperty(window, 'history', {
      value: {
        state: null,
        length: 1,
      },
      writable: true,
      configurable: true,
    });
  });

  afterEach(() => {
    vi.restoreAllMocks();
    eventListeners = {};

    // Restore original window properties
    Object.defineProperty(window, 'location', {
      value: originalLocation,
      writable: true,
      configurable: true,
    });

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

  it('should return location state after mount', () => {
    const { result } = renderHook(() => useLocation());

    // After mount effect
    expect(result.current).toEqual({
      trigger: 'load',
      state: null,
      length: 1,
      hash: '',
      host: 'localhost:3000',
      hostname: 'localhost',
      href: 'http://localhost:3000/',
      origin: 'http://localhost:3000',
      pathname: '/',
      port: '3000',
      protocol: 'http:',
      search: '',
    });
  });

  it('should add event listeners on mount', () => {
    renderHook(() => useLocation());

    expect(window.addEventListener).toHaveBeenCalledWith(
      'popstate',
      expect.any(Function),
      { passive: true }
    );
    expect(window.addEventListener).toHaveBeenCalledWith(
      'hashchange',
      expect.any(Function),
      { passive: true }
    );
  });

  it('should update state on popstate event', () => {
    const { result } = renderHook(() => useLocation());

    // Initial state
    expect(result.current?.trigger).toBe('load');

    // Simulate popstate event
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/new-path',
          href: 'http://localhost:3000/new-path',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.trigger).toBe('popstate');
    expect(result.current?.pathname).toBe('/new-path');
    expect(result.current?.href).toBe('http://localhost:3000/new-path');
  });

  it('should update state on hashchange event', () => {
    const { result } = renderHook(() => useLocation());

    // Initial state
    expect(result.current?.trigger).toBe('load');
    expect(result.current?.hash).toBe('');

    // Simulate hashchange event
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hash: '#section',
          href: 'http://localhost:3000/#section',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['hashchange']?.forEach((handler) => {
        handler(new Event('hashchange'));
      });
    });

    expect(result.current?.trigger).toBe('hashchange');
    expect(result.current?.hash).toBe('#section');
    expect(result.current?.href).toBe('http://localhost:3000/#section');
  });

  it('should handle multiple popstate events', () => {
    const { result } = renderHook(() => useLocation());

    // First popstate
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/page1',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.pathname).toBe('/page1');
    expect(result.current?.trigger).toBe('popstate');

    // Second popstate
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/page2',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.pathname).toBe('/page2');
    expect(result.current?.trigger).toBe('popstate');
  });

  it('should handle multiple hashchange events', () => {
    const { result } = renderHook(() => useLocation());

    // First hashchange
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hash: '#section1',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['hashchange']?.forEach((handler) => {
        handler(new Event('hashchange'));
      });
    });

    expect(result.current?.hash).toBe('#section1');
    expect(result.current?.trigger).toBe('hashchange');

    // Second hashchange
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hash: '#section2',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['hashchange']?.forEach((handler) => {
        handler(new Event('hashchange'));
      });
    });

    expect(result.current?.hash).toBe('#section2');
    expect(result.current?.trigger).toBe('hashchange');
  });

  it('should handle interleaved popstate and hashchange events', () => {
    const { result } = renderHook(() => useLocation());

    // Popstate
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/new-page',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.trigger).toBe('popstate');
    expect(result.current?.pathname).toBe('/new-page');

    // Hashchange
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hash: '#anchor',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['hashchange']?.forEach((handler) => {
        handler(new Event('hashchange'));
      });
    });

    expect(result.current?.trigger).toBe('hashchange');
    expect(result.current?.hash).toBe('#anchor');
  });

  it('should update history state on popstate', () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      Object.defineProperty(window, 'history', {
        value: {
          state: { page: 'new' },
          length: 2,
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.state).toEqual({ page: 'new' });
    expect(result.current?.length).toBe(2);
  });

  it('should update search params on popstate', () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          search: '?foo=bar&baz=qux',
          href: 'http://localhost:3000/?foo=bar&baz=qux',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.search).toBe('?foo=bar&baz=qux');
  });

  it('should remove event listeners on unmount', () => {
    const { unmount } = renderHook(() => useLocation());

    const popstateHandlers = [...(eventListeners['popstate'] || [])];
    const hashchangeHandlers = [...(eventListeners['hashchange'] || [])];

    expect(popstateHandlers.length).toBe(1);
    expect(hashchangeHandlers.length).toBe(1);

    unmount();

    expect(window.removeEventListener).toHaveBeenCalledWith(
      'popstate',
      popstateHandlers[0]
    );
    expect(window.removeEventListener).toHaveBeenCalledWith(
      'hashchange',
      hashchangeHandlers[0]
    );
  });

  it('should not update after unmount', () => {
    const { result, unmount } = renderHook(() => useLocation());

    const initialState = result.current;

    unmount();

    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/should-not-update',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    // State should not change after unmount
    expect(result.current).toEqual(initialState);
  });

  it('should handle changes to all location properties', () => {
    const { result } = renderHook(() => useLocation());

    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          hash: '#new-hash',
          host: 'example.com:8080',
          hostname: 'example.com',
          href: 'https://example.com:8080/new-path?q=test#new-hash',
          origin: 'https://example.com:8080',
          pathname: '/new-path',
          port: '8080',
          protocol: 'https:',
          search: '?q=test',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current).toEqual({
      trigger: 'popstate',
      state: null,
      length: 1,
      hash: '#new-hash',
      host: 'example.com:8080',
      hostname: 'example.com',
      href: 'https://example.com:8080/new-path?q=test#new-hash',
      origin: 'https://example.com:8080',
      pathname: '/new-path',
      port: '8080',
      protocol: 'https:',
      search: '?q=test',
    });
  });

  it('should work with standard browser navigation patterns', () => {
    const { result } = renderHook(() => useLocation());

    // Initial load
    expect(result.current?.trigger).toBe('load');

    // Simulate pushState (would normally trigger popstate on back/forward)
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/step1',
          href: 'http://localhost:3000/step1',
        },
        writable: true,
        configurable: true,
      });

      Object.defineProperty(window, 'history', {
        value: {
          state: { step: 1 },
          length: 2,
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    expect(result.current?.pathname).toBe('/step1');
    expect(result.current?.state).toEqual({ step: 1 });
    expect(result.current?.trigger).toBe('popstate');

    // Simulate hash navigation
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          hash: '#content',
          href: 'http://localhost:3000/step1#content',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['hashchange']?.forEach((handler) => {
        handler(new Event('hashchange'));
      });
    });

    expect(result.current?.hash).toBe('#content');
    expect(result.current?.trigger).toBe('hashchange');
  });

  it('should maintain separate state for different hook instances', () => {
    const { result: result1 } = renderHook(() => useLocation());
    const { result: result2 } = renderHook(() => useLocation());

    // Both should have the same initial state
    expect(result1.current).toEqual(result2.current);

    // Trigger location change
    act(() => {
      Object.defineProperty(window, 'location', {
        value: {
          ...window.location,
          pathname: '/updated',
        },
        writable: true,
        configurable: true,
      });

      eventListeners['popstate']?.forEach((handler) => {
        handler(new Event('popstate'));
      });
    });

    // Both instances should update
    expect(result1.current?.pathname).toBe('/updated');
    expect(result2.current?.pathname).toBe('/updated');
    expect(result1.current).toEqual(result2.current);
  });

  it('should handle rapid location changes', () => {
    const { result } = renderHook(() => useLocation());

    // Simulate rapid navigation
    act(() => {
      for (let i = 1; i <= 5; i++) {
        Object.defineProperty(window, 'location', {
          value: {
            ...window.location,
            pathname: `/page${i}`,
            href: `http://localhost:3000/page${i}`,
          },
          writable: true,
          configurable: true,
        });

        eventListeners['popstate']?.forEach((handler) => {
          handler(new Event('popstate'));
        });
      }
    });

    // Should end up at the last location
    expect(result.current?.pathname).toBe('/page5');
    expect(result.current?.href).toBe('http://localhost:3000/page5');
  });
});
