import { renderHook } from '@testing-library/react';
import { useModelDataLoader } from '../useModelDataLoader';

// Mock store state
const mockSetNodes = jest.fn();
const mockSetEdges = jest.fn();
const mockSetComponents = jest.fn();
const mockSetFavoriteComponents = jest.fn();
const mockSetContexts = jest.fn();
const mockSetSelectedContextId = jest.fn();
const mockSetContextConfig = jest.fn();
const mockSetModelData = jest.fn();
const mockSetSelectedNode = jest.fn();
const mockSetDependencies = jest.fn();
const mockSetVisibleStartNodeIds = jest.fn();

jest.mock('../../store/store', () => ({
  useStore: jest.fn((selector) => {
    const state = {
      setNodes: mockSetNodes,
      setEdges: mockSetEdges,
      setComponents: mockSetComponents,
      setFavoriteComponents: mockSetFavoriteComponents,
      setContexts: mockSetContexts,
      setDependencies: mockSetDependencies,
      setSelectedContextId: mockSetSelectedContextId,
      setContextConfig: mockSetContextConfig,
      setModelData: mockSetModelData,
      setSelectedNode: mockSetSelectedNode,
      setVisibleStartNodeIds: mockSetVisibleStartNodeIds,
    };
    return selector(state);
  }),
}));

// Mock parseModelData
jest.mock('../../utils/modelUtils', () => ({
  parseModelData: jest.fn((data) => ({
    modelData: data,
    nodes: data.nodes || [],
    edges: data.edges || [],
  })),
}));

describe('useModelDataLoader', () => {
  let mockSetViewportTarget: jest.Mock;

  beforeEach(() => {
    jest.clearAllMocks();
    mockSetViewportTarget = jest.fn();
    mockSetFavoriteComponents.mockClear();
    // Reset document query selector mock
    document.querySelector = jest.fn(() => null);
  });

  const renderUseModelDataLoader = (settings: any = {}) => {
    return renderHook(() =>
      useModelDataLoader({
        settings,
        setViewportTarget: mockSetViewportTarget,
      })
    );
  };

  describe('replayData processing', () => {
    it('should return empty array when no replayData in settings', () => {
      const { result } = renderUseModelDataLoader({});
      expect(result.current.replayData).toEqual([]);
    });

    it('should return replayData from settings when provided', () => {
      const replayData = [{ step: 1 }, { step: 2 }];
      const { result } = renderUseModelDataLoader({
        modeler: { replayData },
      });
      expect(result.current.replayData).toEqual(replayData);
    });
  });

  describe('component loading', () => {
    it('should load components from settings.modeler.components', () => {
      const components = [
        { plugin: 'action1', label: 'Action 1', category: 'Actions' },
        { plugin: 'event1', label: 'Event 1', category: 'Events' },
      ];

      renderUseModelDataLoader({
        modeler: { components },
      });

      expect(mockSetComponents).toHaveBeenCalled();
      const loadedComponents = mockSetComponents.mock.calls[0][0];
      expect(loadedComponents.length).toBeGreaterThanOrEqual(2);
    });

    it('should add gateway component if not present', () => {
      const components = [
        { plugin: 'action1', label: 'Action 1', category: 'Actions' },
      ];

      renderUseModelDataLoader({
        modeler: { components },
      });

      const loadedComponents = mockSetComponents.mock.calls[0][0];
      const gateway = loadedComponents.find((c: any) => c.plugin === 'gateway');
      expect(gateway).toBeDefined();
      expect(gateway.category).toBe('Others');
    });

    it('should not duplicate gateway if already present', () => {
      const components = [
        { plugin: 'gateway', label: 'Gateway', category: 'Others' },
        { plugin: 'action1', label: 'Action 1', category: 'Actions' },
      ];

      renderUseModelDataLoader({
        modeler: { components },
      });

      const loadedComponents = mockSetComponents.mock.calls[0][0];
      const gateways = loadedComponents.filter((c: any) => c.plugin === 'gateway');
      expect(gateways.length).toBe(1);
    });

    it('should convert ownerComponents to flat array when components not provided', () => {
      const ownerComponents = {
        Actions: [
          { plugin: 'action1', label: 'Action 1' },
          { plugin: 'action2', label: 'Action 2' },
        ],
        Events: [
          { plugin: 'event1', label: 'Event 1' },
        ],
      };

      renderUseModelDataLoader({
        ownerComponents,
      });

      expect(mockSetComponents).toHaveBeenCalled();
      const loadedComponents = mockSetComponents.mock.calls[0][0];

      // Should include components from both categories plus gateway
      const action1 = loadedComponents.find((c: any) => c.plugin === 'action1');
      expect(action1).toBeDefined();
      expect(action1.category).toBe('Actions');

      const event1 = loadedComponents.find((c: any) => c.plugin === 'event1');
      expect(event1).toBeDefined();
      expect(event1.category).toBe('Events');
    });

    it('should add gateway when using ownerComponents', () => {
      const ownerComponents = {
        Actions: [{ plugin: 'action1', label: 'Action 1' }],
      };

      renderUseModelDataLoader({
        ownerComponents,
      });

      const loadedComponents = mockSetComponents.mock.calls[0][0];
      const gateway = loadedComponents.find((c: any) => c.plugin === 'gateway');
      expect(gateway).toBeDefined();
    });

    it('should set only gateway when no components provided', () => {
      renderUseModelDataLoader({});

      expect(mockSetComponents).toHaveBeenCalled();
      const loadedComponents = mockSetComponents.mock.calls[0][0];
      expect(loadedComponents.length).toBe(1);
      expect(loadedComponents[0].plugin).toBe('gateway');
    });
  });

  describe('favorite components loading', () => {
    it('should load favorite components from settings', () => {
      const favoriteComponents = {
        1: ['test_base:test_custom', 'content_entity:update'],
        4: ['action_message_action', 'test_token_set_value'],
        5: ['test_scalar', 'test_count'],
      };

      renderUseModelDataLoader({
        modeler_api: { favorite_components: favoriteComponents },
      });

      expect(mockSetFavoriteComponents).toHaveBeenCalledWith(favoriteComponents);
    });

    it('should not call setFavoriteComponents when not provided', () => {
      renderUseModelDataLoader({});

      expect(mockSetFavoriteComponents).not.toHaveBeenCalled();
    });
  });

  describe('model data loading', () => {
    it('should parse and load model data from settings', () => {
      const modelData = JSON.stringify({
        id: 'model-1',
        nodes: [
          { id: 'node-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      expect(mockSetNodes).toHaveBeenCalled();
      expect(mockSetEdges).toHaveBeenCalled();
    });

    it('should ensure all nodes have data property', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'node-1', position: { x: 100, y: 100 } }, // No data property
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData },
      });

      const setNodesCall = mockSetNodes.mock.calls[0][0];
      expect(setNodesCall[0].data).toBeDefined();
    });

    it('should handle model data parsing errors gracefully', () => {
      const consoleSpy = jest.spyOn(console, 'error').mockImplementation();

      renderUseModelDataLoader({
        modeler: { modelData: 'invalid json' },
      });

      expect(consoleSpy).toHaveBeenCalledWith(
        'Failed to parse model data from settings:',
        expect.any(Error)
      );
      consoleSpy.mockRestore();
    });

    it('should try loading from hidden field when no modelData in settings', () => {
      const mockHiddenField = {
        value: JSON.stringify({
          nodes: [{ id: 'node-1', position: { x: 0, y: 0 } }],
          edges: [],
        }),
      };
      document.querySelector = jest.fn(() => mockHiddenField);

      renderUseModelDataLoader({});

      expect(document.querySelector).toHaveBeenCalledWith('[name="modeler_api_data"]');
      expect(mockSetNodes).toHaveBeenCalled();
    });

    it('should initialize default model when no data found anywhere', () => {
      document.querySelector = jest.fn(() => null);

      renderUseModelDataLoader({});

      expect(mockSetModelData).toHaveBeenCalled();
      const defaultModel = mockSetModelData.mock.calls[0][0];
      expect(defaultModel.version).toBe('1.0.0');
      expect(defaultModel.metadata.label).toBe('New Workflow');
    });

    it('should use modelId from settings.modeler.modelId', () => {
      const modelData = JSON.stringify({
        id: 'original-id',
        nodes: [],
        edges: [],
        metadata: { label: 'Test Model' },
      });

      renderUseModelDataLoader({
        modeler: { modelData, modelId: 'new-model-id' },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      const loadedModel = mockSetModelData.mock.calls[0][0];
      expect(loadedModel.id).toBe('new-model-id');
    });

    it('should fall back to model data id when modelId not provided', () => {
      const modelData = JSON.stringify({
        id: 'original-id',
        nodes: [],
        edges: [],
        metadata: { label: 'Test Model' },
      });

      renderUseModelDataLoader({
        modeler: { modelData },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      const loadedModel = mockSetModelData.mock.calls[0][0];
      expect(loadedModel.id).toBe('original-id');
    });

    it('should merge metadata from settings.modeler_api.metadata', () => {
      const modelData = JSON.stringify({
        id: 'model-1',
        nodes: [],
        edges: [],
        metadata: { label: 'Original Label', description: 'Original Description' },
      });

      renderUseModelDataLoader({
        modeler: { modelData },
        modeler_api: {
          metadata: {
            label: 'API Label',
            tags: ['tag1', 'tag2'],
          },
        },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      const loadedModel = mockSetModelData.mock.calls[0][0];
      // API metadata should override
      expect(loadedModel.metadata.label).toBe('API Label');
      expect(loadedModel.metadata.tags).toEqual(['tag1', 'tag2']);
      // Original values not in API should be preserved (if parseModelData returned them)
    });

    it('should use modelId in default model when no data found', () => {
      document.querySelector = jest.fn(() => null);

      renderUseModelDataLoader({
        modeler: { modelId: 'preset-model-id' },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      const defaultModel = mockSetModelData.mock.calls[0][0];
      expect(defaultModel.id).toBe('preset-model-id');
    });

    it('should use api metadata in default model when no data found', () => {
      document.querySelector = jest.fn(() => null);

      renderUseModelDataLoader({
        modeler_api: {
          metadata: {
            label: 'Custom Default Label',
            description: 'Custom Description',
            executable: false,
            tags: ['default-tag'],
          },
        },
      });

      expect(mockSetModelData).toHaveBeenCalled();
      const defaultModel = mockSetModelData.mock.calls[0][0];
      expect(defaultModel.metadata.label).toBe('Custom Default Label');
      expect(defaultModel.metadata.description).toBe('Custom Description');
      expect(defaultModel.metadata.executable).toBe(false);
      expect(defaultModel.metadata.tags).toEqual(['default-tag']);
    });
  });

  describe('auto-selection', () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.useRealTimers();
    });

    it('should auto-select node when selectComponentId is specified', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'node-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } },
          { id: 'node-2', position: { x: 200, y: 100 }, data: { label: 'Node 2' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'node-1' },
      });

      // Run timers to trigger selection
      jest.runAllTimers();

      expect(mockSetSelectedNode).toHaveBeenCalled();
    });

    it('should set viewport target for event nodes with top alignment', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'event-1', type: 'start', position: { x: 100, y: 100 }, data: { label: 'Event', category: 'Events' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'event-1' },
      });

      jest.runAllTimers();

      expect(mockSetViewportTarget).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'top-align',
          nodeId: 'event-1',
        })
      );
    });

    it('should set viewport target with center for non-event nodes', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'action-1', position: { x: 100, y: 100 }, data: { label: 'Action', category: 'Actions' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'action-1' },
      });

      jest.runAllTimers();

      expect(mockSetViewportTarget).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'center',
          nodeId: 'action-1',
        })
      );
    });

    it('should fit view when no selectComponentId but nodes exist', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'node-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData },
      });

      jest.runAllTimers();

      expect(mockSetViewportTarget).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'fit',
        })
      );
    });

    it('should not select node when selectComponentId does not match any node', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'node-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'non-existent' },
      });

      jest.runAllTimers();

      // Should not call setSelectedNode for non-existent node
      // It will still try to set nodes with selection marker but won't find the node
      expect(mockSetSelectedNode).not.toHaveBeenCalled();
    });

    it('should set visibleStartNodeIds when selecting a start node with multiple start nodes', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'event-1', type: 'start', position: { x: 0, y: 0 }, data: { label: 'Event 1' } },
          { id: 'event-2', type: 'start', position: { x: 200, y: 0 }, data: { label: 'Event 2' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'event-1' },
      });

      expect(mockSetVisibleStartNodeIds).toHaveBeenCalledWith(['event-1']);
    });

    it('should not set visibleStartNodeIds when selecting a non-start node', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'event-1', type: 'start', position: { x: 0, y: 0 }, data: { label: 'Event 1' } },
          { id: 'action-1', position: { x: 200, y: 0 }, data: { label: 'Action 1' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'action-1' },
      });

      expect(mockSetVisibleStartNodeIds).not.toHaveBeenCalled();
    });

    it('should not set visibleStartNodeIds when there is only one start node', () => {
      const modelData = JSON.stringify({
        nodes: [
          { id: 'event-1', type: 'start', position: { x: 0, y: 0 }, data: { label: 'Event 1' } },
          { id: 'action-1', position: { x: 200, y: 0 }, data: { label: 'Action 1' } },
        ],
        edges: [],
      });

      renderUseModelDataLoader({
        modeler: { modelData, selectComponentId: 'event-1' },
      });

      expect(mockSetVisibleStartNodeIds).not.toHaveBeenCalled();
    });
  });

  describe('context config loading', () => {
    it('should load context config from settings', () => {
      const contextConfig = { event: 'entity:insert', type: 'node' };

      renderUseModelDataLoader({
        modeler: { setContextConfig: contextConfig },
      });

      expect(mockSetContextConfig).toHaveBeenCalledWith(contextConfig);
    });

    it('should not call setContextConfig when not provided', () => {
      renderUseModelDataLoader({});

      expect(mockSetContextConfig).not.toHaveBeenCalled();
    });

    it('should not call setContextConfig when value is not an object', () => {
      renderUseModelDataLoader({
        modeler: { setContextConfig: 'invalid' as any },
      });

      expect(mockSetContextConfig).not.toHaveBeenCalled();
    });

    it('should handle empty context config object', () => {
      renderUseModelDataLoader({
        modeler: { setContextConfig: {} },
      });

      expect(mockSetContextConfig).toHaveBeenCalledWith({});
    });

    it('should load context config alongside other settings', () => {
      const contextConfig = { field1: 'value1' };
      const favoriteComponents = { 1: ['plugin_a'] };

      renderUseModelDataLoader({
        modeler: {
          setContextConfig: contextConfig,
        },
        modeler_api: {
          favorite_components: favoriteComponents,
        },
      });

      expect(mockSetContextConfig).toHaveBeenCalledWith(contextConfig);
      expect(mockSetFavoriteComponents).toHaveBeenCalledWith(favoriteComponents);
    });
  });

  describe('context auto-selection', () => {
    it('should auto-select context when selectContextId matches an available context', () => {
      renderUseModelDataLoader({
        modeler: {
          selectContextId: 'ctx_1',
        },
        modeler_api: {
          contexts: [
            { id: 'ctx_1', topic: 'Content', model_owner: 'test_owner', components: {} },
            { id: 'ctx_2', topic: 'User', model_owner: 'test_owner', components: {} },
          ],
        },
      });

      expect(mockSetContexts).toHaveBeenCalledWith([
        { id: 'ctx_1', topic: 'Content', model_owner: 'test_owner', components: {} },
        { id: 'ctx_2', topic: 'User', model_owner: 'test_owner', components: {} },
      ]);
      expect(mockSetSelectedContextId).toHaveBeenCalledWith('ctx_1');
    });

    it('should not auto-select context when selectContextId does not match any context', () => {
      renderUseModelDataLoader({
        modeler: {
          selectContextId: 'ctx_nonexistent',
        },
        modeler_api: {
          contexts: [
            { id: 'ctx_1', topic: 'Content', model_owner: 'test_owner', components: {} },
          ],
        },
      });

      expect(mockSetContexts).toHaveBeenCalled();
      expect(mockSetSelectedContextId).not.toHaveBeenCalled();
    });

    it('should not auto-select context when selectContextId is not provided', () => {
      renderUseModelDataLoader({
        modeler_api: {
          contexts: [
            { id: 'ctx_1', topic: 'Content', model_owner: 'test_owner', components: {} },
          ],
        },
      });

      expect(mockSetContexts).toHaveBeenCalled();
      expect(mockSetSelectedContextId).not.toHaveBeenCalled();
    });

    it('should not auto-select context when no contexts are available', () => {
      renderUseModelDataLoader({
        modeler: {
          selectContextId: 'ctx_1',
        },
      });

      expect(mockSetSelectedContextId).not.toHaveBeenCalled();
    });
  });

  describe('dependency loading', () => {
    it('should load dependencies from settings.modeler_api.dependencies', () => {
      const dependencies = {
        link: {
          'test_route_match': [{ type: 'start' as const, id: 'kernel:controller' }],
        },
        element: {
          'test_form_add_textfield': [{ type: 'start' as const, id: 'form:form_build' }],
        },
      };

      renderUseModelDataLoader({
        modeler_api: { dependencies },
      });

      expect(mockSetDependencies).toHaveBeenCalledWith(dependencies);
    });

    it('should not call setDependencies when not provided', () => {
      renderUseModelDataLoader({});

      expect(mockSetDependencies).not.toHaveBeenCalled();
    });

    it('should not call setDependencies when dependencies is not an object', () => {
      renderUseModelDataLoader({
        modeler_api: { dependencies: 'invalid' },
      });

      expect(mockSetDependencies).not.toHaveBeenCalled();
    });
  });
});
