/**
 * EditorContext Tests
 *
 * Tests for EditorContext, EditorProvider, useEditorContext, and withEditorContext
 */

import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import EditorContext, {
  EditorProvider,
  useEditorContext,
  withEditorContext
} from '../EditorContext';

describe('EditorContext', () => {
  // Mock editor context value
  const createMockEditorContext = (overrides = {}) => ({
    content: {
      html: '<p>Test</p>',
      updateContent: jest.fn(),
      ...overrides.content
    },
    layout: {
      splitSize: 50,
      viewMode: 'split',
      ...overrides.layout
    },
    validation: {
      isValid: true,
      errors: [],
      ...overrides.validation
    },
    preferences: {
      theme: 'light',
      autoSave: true,
      ...overrides.preferences
    },
    editorRef: { current: null },
    handleLabelInsert: jest.fn(),
    handleSave: jest.fn(),
    readOnly: false,
    ...overrides
  });

  describe('EditorContext', () => {
    it('has correct display name', () => {
      expect(EditorContext.displayName).toBe('EditorContext');
    });

    it('is created as a context object', () => {
      expect(EditorContext).toBeDefined();
      expect(EditorContext.Provider).toBeDefined();
      expect(EditorContext.Consumer).toBeDefined();
    });
  });

  describe('EditorProvider', () => {
    it('renders children correctly', () => {
      const mockValue = createMockEditorContext();

      render(
        <EditorProvider value={mockValue}>
          <div data-testid="child">Test Child</div>
        </EditorProvider>
      );

      expect(screen.getByTestId('child')).toBeInTheDocument();
      expect(screen.getByTestId('child')).toHaveTextContent('Test Child');
    });

    it('renders multiple children', () => {
      const mockValue = createMockEditorContext();

      render(
        <EditorProvider value={mockValue}>
          <div data-testid="child-1">Child 1</div>
          <div data-testid="child-2">Child 2</div>
          <div data-testid="child-3">Child 3</div>
        </EditorProvider>
      );

      expect(screen.getByTestId('child-1')).toBeInTheDocument();
      expect(screen.getByTestId('child-2')).toBeInTheDocument();
      expect(screen.getByTestId('child-3')).toBeInTheDocument();
    });

    it('provides context value to children', () => {
      const mockValue = createMockEditorContext();

      const TestComponent = () => {
        const context = useEditorContext();
        return <div data-testid="context-content">{context.content.html}</div>;
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('context-content')).toHaveTextContent('<p>Test</p>');
    });

    it('memoizes context value', () => {
      const mockValue = createMockEditorContext();
      let renderCount = 0;

      const TestComponent = () => {
        useEditorContext();
        renderCount++;
        return <div>Rendered {renderCount} times</div>;
      };

      const { rerender } = render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(renderCount).toBe(1);

      // Rerender with same value
      rerender(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      // Should not trigger re-render due to memoization
      expect(renderCount).toBe(2); // Context provider itself re-renders
    });

    it('updates context when value changes', () => {
      const mockValue1 = createMockEditorContext({ content: { html: '<p>Test 1</p>' } });
      const mockValue2 = createMockEditorContext({ content: { html: '<p>Test 2</p>' } });

      const TestComponent = () => {
        const context = useEditorContext();
        return <div data-testid="content">{context.content.html}</div>;
      };

      const { rerender } = render(
        <EditorProvider value={mockValue1}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('<p>Test 1</p>');

      rerender(
        <EditorProvider value={mockValue2}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('<p>Test 2</p>');
    });

    it('handles nested providers', () => {
      const outerValue = createMockEditorContext({ content: { html: 'Outer' } });
      const innerValue = createMockEditorContext({ content: { html: 'Inner' } });

      const TestComponent = ({ testId }) => {
        const context = useEditorContext();
        return <div data-testid={testId}>{context.content.html}</div>;
      };

      render(
        <EditorProvider value={outerValue}>
          <TestComponent testId="outer" />
          <EditorProvider value={innerValue}>
            <TestComponent testId="inner" />
          </EditorProvider>
        </EditorProvider>
      );

      expect(screen.getByTestId('outer')).toHaveTextContent('Outer');
      expect(screen.getByTestId('inner')).toHaveTextContent('Inner');
    });

    it('handles null children', () => {
      const mockValue = createMockEditorContext();

      expect(() => {
        render(
          <EditorProvider value={mockValue}>
            {null}
          </EditorProvider>
        );
      }).not.toThrow();
    });

    it('handles fragment children', () => {
      const mockValue = createMockEditorContext();

      render(
        <EditorProvider value={mockValue}>
          <>
            <div data-testid="fragment-1">Fragment 1</div>
            <div data-testid="fragment-2">Fragment 2</div>
          </>
        </EditorProvider>
      );

      expect(screen.getByTestId('fragment-1')).toBeInTheDocument();
      expect(screen.getByTestId('fragment-2')).toBeInTheDocument();
    });
  });

  describe('useEditorContext', () => {
    it('returns context value when used within EditorProvider', () => {
      const mockValue = createMockEditorContext();

      const TestComponent = () => {
        const context = useEditorContext();
        return <div data-testid="test">{JSON.stringify(context.content.html)}</div>;
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('test')).toHaveTextContent('<p>Test</p>');
    });

    it('throws error when used outside of EditorProvider', () => {
      // Suppress console.error for this test
      const originalError = console.error;
      console.error = jest.fn();

      const TestComponent = () => {
        useEditorContext();
        return <div>Should not render</div>;
      };

      expect(() => {
        render(<TestComponent />);
      }).toThrow('useEditorContext must be used within an EditorProvider');

      console.error = originalError;
    });

    it('provides access to all context properties', () => {
      const mockValue = createMockEditorContext();
      let capturedContext;

      const TestComponent = () => {
        capturedContext = useEditorContext();
        return <div>Test</div>;
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(capturedContext).toHaveProperty('content');
      expect(capturedContext).toHaveProperty('layout');
      expect(capturedContext).toHaveProperty('validation');
      expect(capturedContext).toHaveProperty('preferences');
      expect(capturedContext).toHaveProperty('editorRef');
      expect(capturedContext).toHaveProperty('handleLabelInsert');
      expect(capturedContext).toHaveProperty('handleSave');
      expect(capturedContext).toHaveProperty('readOnly');
    });

    it('provides access to nested context properties', () => {
      const mockValue = createMockEditorContext({
        content: { html: '<p>Test</p>', updateContent: jest.fn() },
        layout: { splitSize: 60, viewMode: 'code' }
      });

      const TestComponent = () => {
        const { content, layout } = useEditorContext();
        return (
          <div>
            <div data-testid="html">{content.html}</div>
            <div data-testid="split-size">{layout.splitSize}</div>
          </div>
        );
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('html')).toHaveTextContent('<p>Test</p>');
      expect(screen.getByTestId('split-size')).toHaveTextContent('60');
    });

    it('can call context functions', () => {
      const mockHandleSave = jest.fn();
      const mockValue = createMockEditorContext({ handleSave: mockHandleSave });

      const TestComponent = () => {
        const { handleSave } = useEditorContext();
        return <button onClick={handleSave} data-testid="save-btn">Save</button>;
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      const saveButton = screen.getByTestId('save-btn');
      saveButton.click();

      expect(mockHandleSave).toHaveBeenCalledTimes(1);
    });
  });

  describe('withEditorContext HOC', () => {
    it('wraps component and injects editor context', () => {
      const mockValue = createMockEditorContext();

      const TestComponent = ({ editorContext }) => (
        <div data-testid="content">{editorContext.content.html}</div>
      );

      const WrappedComponent = withEditorContext(TestComponent);

      render(
        <EditorProvider value={mockValue}>
          <WrappedComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('<p>Test</p>');
    });

    it('passes through original props', () => {
      const mockValue = createMockEditorContext();

      const TestComponent = ({ customProp, editorContext }) => (
        <div>
          <div data-testid="custom">{customProp}</div>
          <div data-testid="context">{editorContext.content.html}</div>
        </div>
      );

      const WrappedComponent = withEditorContext(TestComponent);

      render(
        <EditorProvider value={mockValue}>
          <WrappedComponent customProp="Custom Value" />
        </EditorProvider>
      );

      expect(screen.getByTestId('custom')).toHaveTextContent('Custom Value');
      expect(screen.getByTestId('context')).toHaveTextContent('<p>Test</p>');
    });

    it('sets correct displayName with component name', () => {
      const TestComponent = () => <div>Test</div>;
      TestComponent.displayName = 'TestComponent';

      const WrappedComponent = withEditorContext(TestComponent);

      expect(WrappedComponent.displayName).toBe('withEditorContext(TestComponent)');
    });

    it('sets correct displayName with function name', () => {
      function MyComponent() {
        return <div>Test</div>;
      }

      const WrappedComponent = withEditorContext(MyComponent);

      expect(WrappedComponent.displayName).toBe('withEditorContext(MyComponent)');
    });

    it('handles components without displayName', () => {
      const TestComponent = () => <div>Test</div>;

      const WrappedComponent = withEditorContext(TestComponent);

      expect(WrappedComponent.displayName).toContain('withEditorContext');
    });

    it('wrapped component can access all context properties', () => {
      const mockValue = createMockEditorContext();
      let capturedContext;

      const TestComponent = ({ editorContext }) => {
        capturedContext = editorContext;
        return <div>Test</div>;
      };

      const WrappedComponent = withEditorContext(TestComponent);

      render(
        <EditorProvider value={mockValue}>
          <WrappedComponent />
        </EditorProvider>
      );

      expect(capturedContext).toEqual(mockValue);
    });

    it('wrapped component throws error outside provider', () => {
      // Suppress console.error for this test
      const originalError = console.error;
      console.error = jest.fn();

      const TestComponent = () => <div>Test</div>;
      const WrappedComponent = withEditorContext(TestComponent);

      expect(() => {
        render(<WrappedComponent />);
      }).toThrow('useEditorContext must be used within an EditorProvider');

      console.error = originalError;
    });

    it('wrapped component updates when context changes', () => {
      const mockValue1 = createMockEditorContext({ content: { html: 'Value 1' } });
      const mockValue2 = createMockEditorContext({ content: { html: 'Value 2' } });

      const TestComponent = ({ editorContext }) => (
        <div data-testid="content">{editorContext.content.html}</div>
      );

      const WrappedComponent = withEditorContext(TestComponent);

      const { rerender } = render(
        <EditorProvider value={mockValue1}>
          <WrappedComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('Value 1');

      rerender(
        <EditorProvider value={mockValue2}>
          <WrappedComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('Value 2');
    });
  });

  describe('Integration Tests', () => {
    it('multiple components can access same context', () => {
      const mockValue = createMockEditorContext();

      const Component1 = () => {
        const { content } = useEditorContext();
        return <div data-testid="comp-1">{content.html}</div>;
      };

      const Component2 = () => {
        const { layout } = useEditorContext();
        return <div data-testid="comp-2">{layout.splitSize}</div>;
      };

      render(
        <EditorProvider value={mockValue}>
          <Component1 />
          <Component2 />
        </EditorProvider>
      );

      expect(screen.getByTestId('comp-1')).toHaveTextContent('<p>Test</p>');
      expect(screen.getByTestId('comp-2')).toHaveTextContent('50');
    });

    it('combines useEditorContext with other hooks', () => {
      const mockValue = createMockEditorContext();

      const TestComponent = () => {
        const { content } = useEditorContext();
        const [count, setCount] = React.useState(0);

        return (
          <div>
            <div data-testid="content">{content.html}</div>
            <div data-testid="count">{count}</div>
            <button onClick={() => setCount(count + 1)}>Increment</button>
          </div>
        );
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('content')).toHaveTextContent('<p>Test</p>');
      expect(screen.getByTestId('count')).toHaveTextContent('0');
    });

    it('context functions are callable from nested components', () => {
      const mockHandleSave = jest.fn();
      const mockHandleLabelInsert = jest.fn();
      const mockValue = createMockEditorContext({
        handleSave: mockHandleSave,
        handleLabelInsert: mockHandleLabelInsert
      });

      const NestedComponent = () => {
        const { handleSave, handleLabelInsert } = useEditorContext();
        return (
          <div>
            <button onClick={handleSave} data-testid="save">Save</button>
            <button onClick={() => handleLabelInsert('label')} data-testid="insert">Insert</button>
          </div>
        );
      };

      const ParentComponent = () => (
        <div>
          <NestedComponent />
        </div>
      );

      render(
        <EditorProvider value={mockValue}>
          <ParentComponent />
        </EditorProvider>
      );

      screen.getByTestId('save').click();
      screen.getByTestId('insert').click();

      expect(mockHandleSave).toHaveBeenCalledTimes(1);
      expect(mockHandleLabelInsert).toHaveBeenCalledWith('label');
    });
  });

  describe('Edge Cases', () => {
    it('handles undefined values in context gracefully', () => {
      const mockValue = createMockEditorContext({
        editorRef: undefined,
        handleLabelInsert: undefined
      });

      const TestComponent = () => {
        const context = useEditorContext();
        return (
          <div>
            <div data-testid="ref">{String(context.editorRef)}</div>
            <div data-testid="insert">{String(context.handleLabelInsert)}</div>
          </div>
        );
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('ref')).toHaveTextContent('undefined');
      expect(screen.getByTestId('insert')).toHaveTextContent('undefined');
    });

    it('handles readOnly flag correctly', () => {
      const mockValue = createMockEditorContext({ readOnly: true });

      const TestComponent = () => {
        const { readOnly } = useEditorContext();
        return <div data-testid="readonly">{String(readOnly)}</div>;
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('readonly')).toHaveTextContent('true');
    });

    it('handles complex nested objects in context', () => {
      const mockValue = createMockEditorContext({
        validation: {
          isValid: false,
          errors: [
            { line: 1, message: 'Error 1' },
            { line: 2, message: 'Error 2' }
          ],
          warnings: []
        }
      });

      const TestComponent = () => {
        const { validation } = useEditorContext();
        return (
          <div>
            <div data-testid="valid">{String(validation.isValid)}</div>
            <div data-testid="error-count">{validation.errors.length}</div>
          </div>
        );
      };

      render(
        <EditorProvider value={mockValue}>
          <TestComponent />
        </EditorProvider>
      );

      expect(screen.getByTestId('valid')).toHaveTextContent('false');
      expect(screen.getByTestId('error-count')).toHaveTextContent('2');
    });
  });

  describe('PropTypes Validation', () => {
    // Suppress console errors for these tests
    const originalError = console.error;
    beforeAll(() => {
      console.error = jest.fn();
    });
    afterAll(() => {
      console.error = originalError;
    });

    it('accepts valid context value shape', () => {
      const mockValue = createMockEditorContext();

      expect(() => {
        render(
          <EditorProvider value={mockValue}>
            <div>Test</div>
          </EditorProvider>
        );
      }).not.toThrow();
    });

    it('requires children prop', () => {
      const mockValue = createMockEditorContext();

      // This should work
      expect(() => {
        render(
          <EditorProvider value={mockValue}>
            <div>Child</div>
          </EditorProvider>
        );
      }).not.toThrow();
    });

    it('requires value prop', () => {
      // EditorProvider requires value prop with specific shape
      // React will handle the missing prop validation
      expect(EditorProvider.propTypes.value).toBeDefined();
      expect(EditorProvider.propTypes.children).toBeDefined();
    });
  });
});


