/**
 * @fileoverview Tests for OrdoJS Runtime and Hydration
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { OrdoJSHydrator, OrdoJSRuntime } from './index.js';

// Mock DOM environment
const mockElement = {
  getAttribute: vi.fn(),
  setAttribute: vi.fn(),
  removeAttribute: vi.fn(),
  querySelector: vi.fn(),
  querySelectorAll: vi.fn(),
  addEventListener: vi.fn(),
  removeEventListener: vi.fn(),
  textContent: '',
  id: 'test-element'
};

const mockDocument = {
  readyState: 'complete',
  addEventListener: vi.fn(),
  getElementById: vi.fn(),
  querySelector: vi.fn(),
  querySelectorAll: vi.fn()
};

// Mock global document
Object.defineProperty(global, 'document', {
  value: mockDocument,
  writable: true
});

describe('OrdoJSRuntime', () => {
  let runtime: OrdoJSRuntime;

  beforeEach(() => {
    // Reset singleton instance
    (OrdoJSRuntime as any).instance = undefined;
    runtime = OrdoJSRuntime.getInstance();

    // Reset mocks
    vi.clearAllMocks();
  });

  afterEach(() => {
    runtime.unmountAll();
  });

  it('should be a singleton', () => {
    const runtime1 = OrdoJSRuntime.getInstance();
    const runtime2 = OrdoJSRuntime.getInstance();
    expect(runtime1).toBe(runtime2);
  });

  it('should register component constructors', () => {
    const mockConstructor = vi.fn();
    runtime.registerComponent('TestComponent', mockConstructor);

    // Access private property for testing
    const constructors = (runtime as any).componentConstructors;
    expect(constructors.has('TestComponent')).toBe(true);
    expect(constructors.get('TestComponent')).toBe(mockConstructor);
  });

  it('should initialize and auto-hydrate on DOM ready', () => {
    mockDocument.readyState = 'loading';
    mockDocument.querySelectorAll.mockReturnValue([]);
    mockDocument.getElementById.mockReturnValue(null);

    runtime.init();

    expect(mockDocument.addEventListener).toHaveBeenCalledWith(
      'DOMContentLoaded',
      expect.any(Function)
    );
  });

  it('should auto-hydrate immediately if DOM is already ready', () => {
    mockDocument.readyState = 'complete';
    mockDocument.querySelectorAll.mockReturnValue([]);
    mockDocument.getElementById.mockReturnValue(null);

    const autoHydrateSpy = vi.spyOn(runtime as any, 'autoHydrate');

    runtime.init();

    expect(autoHydrateSpy).toHaveBeenCalled();
  });

  it('should hydrate components from hydration data', () => {
    const hydrationData = {
      componentName: 'TestComponent',
      componentId: 'test-123',
      props: { name: 'Test' },
      initialState: { count: 0 },
      version: '1.0'
    };

    const mockHydrationScript = {
      textContent: JSON.stringify(hydrationData)
    };

    const mockComponentElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-ordojs-component') return 'TestComponent';
        if (attr === 'data-component-id') return 'test-123';
        return null;
      }),
      querySelectorAll: vi.fn().mockReturnValue([])
    };

    mockDocument.getElementById.mockReturnValue(mockHydrationScript);
    mockDocument.querySelector.mockReturnValue(mockComponentElement);
    mockDocument.querySelectorAll.mockReturnValue([]);

    const mockConstructor = vi.fn().mockReturnValue({
      id: '',
      name: '',
      element: null,
      props: {},
      state: {},
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn()
    });

    runtime.registerComponent('TestComponent', mockConstructor);
    runtime.autoHydrate();

    expect(mockConstructor).toHaveBeenCalledWith({ name: 'Test' });
    expect(mockComponentElement.setAttribute).toHaveBeenCalledWith('data-ordojs-hydrated', 'true');
  });

  it('should extract props from element attributes', () => {
    const mockComponentElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-props') return '{"title":"Hello","count":42}';
        return null;
      })
    };

    const props = (runtime as any).extractProps(mockComponentElement);

    expect(props).toEqual({ title: 'Hello', count: 42 });
  });

  it('should handle malformed props gracefully', () => {
    const mockComponentElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-props') return 'invalid-json';
        return null;
      })
    };

    const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const props = (runtime as any).extractProps(mockComponentElement);

    expect(props).toEqual({});
    expect(consoleSpy).toHaveBeenCalledWith(
      'Failed to parse props from data-props attribute:',
      expect.any(Error)
    );

    consoleSpy.mockRestore();
  });

  it('should initialize reactive state', () => {
    const mockComponent = {
      id: 'test-123',
      name: 'TestComponent',
      element: mockElement,
      props: {},
      state: {},
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn()
    };

    const initialState = { count: 5, name: 'Test' };

    (runtime as any).initializeReactiveState(mockComponent, initialState);

    expect(mockComponent.state.count).toBe(5);
    expect(mockComponent.state.name).toBe('Test');

    // Test reactivity
    mockComponent.state.count = 10;
    expect(mockComponent.update).toHaveBeenCalled();
  });

  it('should attach event listeners', () => {
    const mockEventElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-event') return 'click';
        return null;
      }),
      addEventListener: vi.fn()
    };

    const mockComponent = {
      id: 'test-123',
      name: 'TestComponent',
      element: mockElement,
      props: {},
      state: {},
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn(),
      handleClick: vi.fn()
    };

    mockElement.querySelectorAll.mockReturnValue([mockEventElement]);

    (runtime as any).attachEventListeners(mockElement, mockComponent);

    expect(mockEventElement.addEventListener).toHaveBeenCalledWith(
      'click',
      expect.any(Function)
    );
  });

  it('should set up interpolation updates', () => {
    const mockInterpolationElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-interpolation') return 'message';
        return null;
      }),
      textContent: ''
    };

    const mockComponent = {
      id: 'test-123',
      name: 'TestComponent',
      element: mockElement,
      props: {},
      state: { message: 'Hello' },
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn()
    };

    mockElement.querySelectorAll.mockReturnValue([mockInterpolationElement]);

    (runtime as any).setupInterpolationUpdates(mockElement, mockComponent);

    // Trigger update
    mockComponent.update();

    expect(mockInterpolationElement.textContent).toBe('Hello');
  });

  it('should get hydrated components', () => {
    const mockComponent = {
      id: 'test-123',
      name: 'TestComponent',
      element: mockElement,
      props: {},
      state: {},
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn()
    };

    // Access private property for testing
    const components = (runtime as any).hydratedComponents;
    components.set('test-123', mockComponent);

    expect(runtime.getComponent('test-123')).toBe(mockComponent);
    expect(runtime.getAllComponents()).toEqual([mockComponent]);
  });

  it('should unmount components', () => {
    const mockComponent = {
      id: 'test-123',
      name: 'TestComponent',
      element: mockElement,
      props: {},
      state: {},
      eventListeners: new Map([['element-click', vi.fn()]]),
      update: vi.fn(),
      unmount: vi.fn()
    };

    // Access private property for testing
    const components = (runtime as any).hydratedComponents;
    components.set('test-123', mockComponent);

    runtime.unmountComponent('test-123');

    expect(mockComponent.unmount).toHaveBeenCalled();
    expect(mockElement.removeAttribute).toHaveBeenCalledWith('data-ordojs-hydrated');
    expect(runtime.getComponent('test-123')).toBeUndefined();
  });
});

describe('OrdoJSHydrator', () => {
  let hydrator: OrdoJSHydrator;
  let runtime: OrdoJSRuntime;

  beforeEach(() => {
    // Reset singleton instance
    (OrdoJSRuntime as any).instance = undefined;
    runtime = OrdoJSRuntime.getInstance();
    hydrator = new OrdoJSHydrator();

    // Reset mocks
    vi.clearAllMocks();
  });

  it('should hydrate a component with component data', () => {
    const mockComponentElement = {
      ...mockElement,
      getAttribute: vi.fn((attr) => {
        if (attr === 'data-ordojs-component') return 'TestComponent';
        if (attr === 'data-component-id') return 'test-123';
        return null;
      })
    };

    const componentData = {
      props: { name: 'Test' },
      state: { count: 0 }
    };

    const mockConstructor = vi.fn().mockReturnValue({
      id: '',
      name: '',
      element: null,
      props: {},
      state: {},
      eventListeners: new Map(),
      update: vi.fn(),
      unmount: vi.fn()
    });

    runtime.registerComponent('TestComponent', mockConstructor);

    const result = hydrator.hydrateComponent(mockComponentElement as any, componentData);

    expect(result).toBeTruthy();
    expect(mockConstructor).toHaveBeenCalledWith({ name: 'Test' });
  });

  it('should warn when element is missing hydration attributes', () => {
    const mockComponentElement = {
      ...mockElement,
      getAttribute: vi.fn(() => null)
    };

    const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const result = hydrator.hydrateComponent(mockComponentElement as any, { props: {} });

    expect(result).toBeNull();
    expect(consoleSpy).toHaveBeenCalledWith('Element missing required hydration attributes');

    consoleSpy.mockRestore();
  });

  it('should attach event listeners manually', () => {
    const mockEventElement = {
      ...mockElement,
      addEventListener: vi.fn()
    };

    mockElement.querySelectorAll.mockReturnValue([mockEventElement]);

    const handlers = {
      click: vi.fn(),
      submit: vi.fn()
    };

    hydrator.attachEventListeners(mockElement as any, handlers);

    expect(mockEventElement.addEventListener).toHaveBeenCalledWith('click', handlers.click);
  });
});
