// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import type {QueryNode} from './query_node';
import {
  type NodeActionHandlers,
  createNodeActions,
  createDeferredNodeActions,
  ensureAllNodeActions,
} from './node_actions';
import {createMockNode} from './query_builder/testing/test_utils';

describe('node_actions', () => {
  let handlers: NodeActionHandlers;
  let addAndConnectTableCalls: Array<{
    tableName: string;
    node: QueryNode;
    portIndex: number;
  }>;
  let insertNodeAtPortCalls: Array<{
    node: QueryNode;
    portIndex: number;
    descriptorKey: string;
  }>;

  beforeEach(() => {
    addAndConnectTableCalls = [];
    insertNodeAtPortCalls = [];
    handlers = {
      onAddAndConnectTable: (tableName, node, portIndex) => {
        addAndConnectTableCalls.push({tableName, node, portIndex});
      },
      onInsertNodeAtPort: (node, portIndex, descriptorKey) => {
        insertNodeAtPortCalls.push({node, portIndex, descriptorKey});
      },
    };
  });

  describe('createNodeActions', () => {
    it('should delegate onAddAndConnectTable to handler with node', () => {
      const node = createMockNode({nodeId: 'test-node'});
      const actions = createNodeActions(node, handlers);

      actions.onAddAndConnectTable?.('my_table', 2);

      expect(addAndConnectTableCalls).toHaveLength(1);
      expect(addAndConnectTableCalls[0].tableName).toBe('my_table');
      expect(addAndConnectTableCalls[0].node).toBe(node);
      expect(addAndConnectTableCalls[0].portIndex).toBe(2);
    });

    it('should delegate onInsertModifyColumnsNode with correct descriptor key', () => {
      const node = createMockNode({nodeId: 'test-node'});
      const actions = createNodeActions(node, handlers);

      actions.onInsertModifyColumnsNode?.(1);

      expect(insertNodeAtPortCalls).toHaveLength(1);
      expect(insertNodeAtPortCalls[0].node).toBe(node);
      expect(insertNodeAtPortCalls[0].portIndex).toBe(1);
      expect(insertNodeAtPortCalls[0].descriptorKey).toBe('modify_columns');
    });

    it('should delegate onInsertCounterToIntervalsNode with correct descriptor key', () => {
      const node = createMockNode({nodeId: 'test-node'});
      const actions = createNodeActions(node, handlers);

      actions.onInsertCounterToIntervalsNode?.(3);

      expect(insertNodeAtPortCalls).toHaveLength(1);
      expect(insertNodeAtPortCalls[0].node).toBe(node);
      expect(insertNodeAtPortCalls[0].portIndex).toBe(3);
      expect(insertNodeAtPortCalls[0].descriptorKey).toBe(
        'counter_to_intervals',
      );
    });
  });

  describe('createDeferredNodeActions', () => {
    it('should not call handler when nodeRef is empty', () => {
      const nodeRef: {current?: QueryNode} = {};
      const actions = createDeferredNodeActions(nodeRef, handlers);

      actions.onAddAndConnectTable?.('my_table', 0);
      actions.onInsertModifyColumnsNode?.(0);
      actions.onInsertCounterToIntervalsNode?.(0);

      expect(addAndConnectTableCalls).toHaveLength(0);
      expect(insertNodeAtPortCalls).toHaveLength(0);
    });

    it('should delegate to handler once nodeRef is set', () => {
      const nodeRef: {current?: QueryNode} = {};
      const actions = createDeferredNodeActions(nodeRef, handlers);

      // Set the node reference after creating actions
      const node = createMockNode({nodeId: 'deferred-node'});
      nodeRef.current = node;

      actions.onAddAndConnectTable?.('table_a', 1);

      expect(addAndConnectTableCalls).toHaveLength(1);
      expect(addAndConnectTableCalls[0].node).toBe(node);
      expect(addAndConnectTableCalls[0].tableName).toBe('table_a');
      expect(addAndConnectTableCalls[0].portIndex).toBe(1);
    });

    it('should delegate onInsertModifyColumnsNode once nodeRef is set', () => {
      const nodeRef: {current?: QueryNode} = {};
      const actions = createDeferredNodeActions(nodeRef, handlers);

      const node = createMockNode({nodeId: 'deferred-node'});
      nodeRef.current = node;

      actions.onInsertModifyColumnsNode?.(5);

      expect(insertNodeAtPortCalls).toHaveLength(1);
      expect(insertNodeAtPortCalls[0].node).toBe(node);
      expect(insertNodeAtPortCalls[0].descriptorKey).toBe('modify_columns');
    });

    it('should delegate onInsertCounterToIntervalsNode once nodeRef is set', () => {
      const nodeRef: {current?: QueryNode} = {};
      const actions = createDeferredNodeActions(nodeRef, handlers);

      const node = createMockNode({nodeId: 'deferred-node'});
      nodeRef.current = node;

      actions.onInsertCounterToIntervalsNode?.(7);

      expect(insertNodeAtPortCalls).toHaveLength(1);
      expect(insertNodeAtPortCalls[0].node).toBe(node);
      expect(insertNodeAtPortCalls[0].descriptorKey).toBe(
        'counter_to_intervals',
      );
    });
  });

  describe('ensureAllNodeActions', () => {
    it('should assign actions to nodes that have none', () => {
      const node1 = createMockNode({nodeId: 'n1'});
      const node2 = createMockNode({nodeId: 'n2'});
      const initializedNodes = new Set<string>();

      ensureAllNodeActions([node1, node2], initializedNodes, handlers);

      expect(node1.context.actions).toBeDefined();
      expect(node2.context.actions).toBeDefined();
      expect(initializedNodes.has('n1')).toBe(true);
      expect(initializedNodes.has('n2')).toBe(true);
    });

    it('should skip nodes already in initializedNodes set', () => {
      const node = createMockNode({nodeId: 'n1'});
      const initializedNodes = new Set<string>(['n1']);

      ensureAllNodeActions([node], initializedNodes, handlers);

      // Actions should not be set because the node was already tracked
      expect(node.context.actions).toBeUndefined();
    });

    it('should preserve existing actions on nodes', () => {
      const existingActions = {
        onAddAndConnectTable: () => {},
      };
      const node = createMockNode({
        nodeId: 'n1',
        context: {actions: existingActions},
      });
      const initializedNodes = new Set<string>();

      ensureAllNodeActions([node], initializedNodes, handlers);

      // Existing actions should be preserved (not overwritten)
      expect(node.context.actions).toBe(existingActions);
      // But node should still be marked as initialized
      expect(initializedNodes.has('n1')).toBe(true);
    });

    it('should handle empty node array', () => {
      const initializedNodes = new Set<string>();
      ensureAllNodeActions([], initializedNodes, handlers);
      expect(initializedNodes.size).toBe(0);
    });

    it('should create working actions that delegate to handlers', () => {
      const node = createMockNode({nodeId: 'n1'});
      const initializedNodes = new Set<string>();

      ensureAllNodeActions([node], initializedNodes, handlers);

      // The created actions should actually work
      node.context.actions?.onAddAndConnectTable?.('test_table', 0);

      expect(addAndConnectTableCalls).toHaveLength(1);
      expect(addAndConnectTableCalls[0].tableName).toBe('test_table');
      expect(addAndConnectTableCalls[0].node).toBe(node);
    });
  });
});
