// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import { describe, it, beforeEach, jest } from '@jest/globals';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { Node } from '@xyflow/react';
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor';
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
import '@testing-library/jest-dom';
import { GoalReduce, type GoalNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx';
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
import { insertGoalInPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditingFunctions.tsx';


// Mock structuredClone
(globalThis as any).structuredClone = jest.fn((val) => JSON.parse(JSON.stringify(val)));

// UUID Regex for checking ID's
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

describe('PlanEditorDialog', () => {
  let user: ReturnType<typeof userEvent.setup>;
  const mockOnSave = jest.fn();

  beforeEach(() => {
    user = userEvent.setup();
    jest.clearAllMocks();
  });

  const defaultPlan: Plan = {
    id: 'plan-1',
    name: 'Test Plan',
    steps: [],
  };

  const extendedPlan: Plan = {
    id: 'extended-plan-1',
    name: 'extended test plan',
    steps: [
      // Step 1: A wave tag gesture
      {
        id: 'firststep',
        type: 'gesture',
        isTag: true,
        gesture: "hello"
      },

      // Step 2: A single tag gesture
      {
        id: 'secondstep',
        type: 'gesture',
        isTag: false,
        gesture: "somefolder/somegesture"
      },

      // Step 3: A LLM action
      {
        id: 'thirdstep',
        type: 'llm',
        goal: 'ask the user something or whatever'
      },

      // Step 4: A speech action
      {
        id: 'fourthstep',
        type: 'speech',
        text: "I'm a cyborg ninja :>"
      },
    ]
  }

  const planWithSteps: Plan = {
    id: 'plan-2',
    name: 'Existing Plan',
    steps: [
      { id: 'step-1', text: 'Hello world', type: 'speech' as const },
      { id: 'step-2', gesture: 'Wave', isTag:true, type: 'gesture' as const },
    ],
  };

  const renderDialog = (props: Partial<React.ComponentProps<typeof PlanEditorDialog>> = {}) => {
    const defaultProps = {
      plan: undefined,
      onSave: mockOnSave,
      description: undefined,
    };

    return renderWithProviders(<PlanEditorDialog {...defaultProps} {...props} />);
  };

  describe('Rendering', () => {
    it('should show "Create Plan" button when no plan is provided', () => {
      renderDialog();
      // The button should be visible
      expect(screen.getByRole('button', { name: 'Create Plan' })).toBeInTheDocument();
      // The dialog content should NOT be visible initially
      expect(screen.queryByText(/Add Action/i)).not.toBeInTheDocument();
    });

    it('should show "Edit Plan" button when a plan is provided', () => {
      renderDialog({ plan: defaultPlan });
      expect(screen.getByRole('button', { name: 'Edit Plan' })).toBeInTheDocument();
    });

    it('should not show "Create Plan" button when a plan exists', () => {
      renderDialog({ plan: defaultPlan });
      // Query for the button text specifically, not dialog title
      expect(screen.queryByRole('button', { name: 'Create Plan' })).not.toBeInTheDocument();
    });
  });

  describe('Dialog Interactions', () => {
    it('should open dialog with "Create Plan" title when creating new plan', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      
      // One for button, one for dialog.
      expect(screen.getAllByText('Create Plan').length).toEqual(2);
    });

    it('should open dialog with "Edit Plan" title when editing existing plan', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      //  One for button, one for dialog
      expect(screen.getAllByText('Edit Plan').length).toEqual(2);
    });

    it('should pre-fill plan name when editing', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
      expect(nameInput.value).toBe(defaultPlan.name);
    });

    it('should close dialog when cancel button is clicked', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      await user.click(screen.getByText('Cancel'));
      
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });
  });

  describe('Plan Creation', () => {
    it('should create a new plan with default values', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      // One for the button, one for the dialog
      expect(screen.getAllByText('Create Plan').length).toEqual(2);

      const nameInput = screen.getByPlaceholderText('Plan name');
      expect(nameInput).toBeInTheDocument();
    });

    it('should auto-fill with description when provided', async () => {
      const description = 'Achieve world peace';
      renderDialog({ description });
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      // Check if plan name is pre-filled with description
      const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
      expect(nameInput.value).toBe(description);
      
      // Check if action type is set to LLM
      const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
      expect(actionTypeSelect.value).toBe('llm');
      
      // Check if suggestion text is shown
      expect(screen.getByText('Filled in as a suggestion!')).toBeInTheDocument();
      expect(screen.getByText('Feel free to change!')).toBeInTheDocument();
    });

    it('should allow changing plan name', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
      const newName = 'My Custom Plan';
      
      // Instead of clear(), select all text and type new value
      await user.click(nameInput);
      await user.keyboard('{Control>}a{/Control}'); // Select all (Ctrl+A)
      await user.keyboard(newName);
      
      expect(nameInput.value).toBe(newName);
    });
  });

  describe('Action Management', () => {
    it('should add a speech action to the plan', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      const actionTypeSelect = screen.getByLabelText(/Action Type/i);
      const actionValueInput = screen.getByPlaceholderText("Speech text")
      const addButton = screen.getByText('Add Step');
      
      // Set up a speech action
      await user.selectOptions(actionTypeSelect, 'speech');
      await user.type(actionValueInput, 'Hello there!');
      
      await user.click(addButton);
      
      // Check if step was added
      expect(screen.getByText('speech:')).toBeInTheDocument();
      expect(screen.getByText('Hello there!')).toBeInTheDocument();
    });

    it('should add a gesture action to the plan', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: /edit plan/i }));
      
      const actionTypeSelect = screen.getByLabelText(/Action Type/i);
      const addButton = screen.getByText('Add Step');
      
      // Set up a gesture action
      await user.selectOptions(actionTypeSelect, 'gesture');
      
      // Find the input field after type change
      const select = screen.getByTestId("tagSelectorTestID")
      const options = within(select).getAllByRole('option')

      await user.selectOptions(select, options[1])
      await user.click(addButton);
      
      // Check if step was added
      expect(screen.getByText('gesture:')).toBeInTheDocument();
    });

    it('should add an LLM action to the plan', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      const actionTypeSelect = screen.getByLabelText(/Action Type/i);
      const addButton = screen.getByText('Add Step');
      
      // Set up an LLM action
      await user.selectOptions(actionTypeSelect, 'llm');
      
      // Find the input field after type change
      const llmInput = screen.getByPlaceholderText(/LLM goal|text/i);
      await user.type(llmInput, 'Generate a story');
      
      await user.click(addButton);
      
      // Check if step was added
      expect(screen.getByText('llm:')).toBeInTheDocument();
      expect(screen.getByText('Generate a story')).toBeInTheDocument();
    });

    it('should disable "Add Step" button when action value is empty', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      const addButton = screen.getByText('Add Step');
      expect(addButton).toBeDisabled();
    });

    it('should reset action form after adding a step', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
    
      const actionValueInput = screen.getByPlaceholderText("Speech text")
      const addButton = screen.getByText('Add Step');
      
      await user.type(actionValueInput, 'Test speech');
      await user.click(addButton);
      
      // Action value should be cleared
      expect(actionValueInput).toHaveValue('');
      // Action type should be reset to speech (default)
      const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
      expect(actionTypeSelect.value).toBe('speech');
    });
  });

  describe('Step Management', () => {
    it('should show existing steps when editing a plan', async () => {
      renderDialog({ plan: planWithSteps });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      // Check if existing steps are shown
      expect(screen.getByText('speech:')).toBeInTheDocument();
      expect(screen.getByText('Hello world')).toBeInTheDocument();
      expect(screen.getByText('gesture:')).toBeInTheDocument();
      expect(screen.getByText('Wave')).toBeInTheDocument();
    });

    it('should show "No steps yet" message when plan has no steps', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      expect(screen.getByText('No steps yet')).toBeInTheDocument();
    });

    it('should remove a step when clicked', async () => {
      renderDialog({ plan: planWithSteps });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      // Initially have 2 steps
      expect(screen.getByText('speech:')).toBeInTheDocument();
      expect(screen.getByText('gesture:')).toBeInTheDocument();
      
      // Click on the first step to remove it
      await user.click(screen.getByText('Hello world'));
      
      // First step should be removed
      expect(screen.queryByText('Hello world')).not.toBeInTheDocument();
      // Second step should still exist
      expect(screen.getByText('Wave')).toBeInTheDocument();
    });
  });

  describe('Save Functionality', () => {
    it('should call onSave with new plan when creating', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      // Set plan name
      const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
      await user.click(nameInput);
      await user.keyboard('{Control>}a{/Control}');
      await user.keyboard('My New Plan');
      
      // Add a step
      const actionValueInput = screen.getByPlaceholderText(/text/i);
      await user.type(actionValueInput, 'First step');
      await user.click(screen.getByText('Add Step'));
      
      // Save the plan
      await user.click(screen.getByText('Create'));
      
      expect(mockOnSave).toHaveBeenCalledWith({
        id: expect.stringMatching(uuidRegex),
        name: 'My New Plan',
        steps: [
          {
            id: expect.stringMatching(uuidRegex),
            text: 'First step',
            type: 'speech',
          },
        ],
      });
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });

    it('should call onSave with updated plan when editing', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      // Change plan name
      const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
      await user.click(nameInput);
      await user.keyboard('{Control>}a{/Control}');
      await user.keyboard('Updated Plan Name');
      
      // Add a step
      const actionValueInput = screen.getByPlaceholderText(/text/i);
      await user.type(actionValueInput, 'New speech action');
      await user.click(screen.getByText('Add Step'));
      
      // Save the plan
      await user.click(screen.getByText('Confirm'));
      
      expect(mockOnSave).toHaveBeenCalledWith({
        id: defaultPlan.id,
        name: 'Updated Plan Name',
        steps: [
          {
            id: expect.stringMatching(uuidRegex),
            text: 'New speech action',
            type: 'speech',
          },
        ],
      });
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });

    it('should call onSave with undefined when reset button is clicked', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      await user.click(screen.getByText('Reset'));
      
      expect(mockOnSave).toHaveBeenCalledWith(undefined);
      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
    });

    it('should disable save button when no draft plan exists', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      // The save button should be enabled since draftPlan exists after clicking Create Plan
      const saveButton = screen.getByText('Create');
      expect(saveButton).not.toBeDisabled();
    });
  });

  describe('Step Indexing', () => {
    it('should show correct step numbers', async () => {
      renderDialog({ plan: defaultPlan });
      
      await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
      
      // Add multiple steps
      const actionValueInput = screen.getByPlaceholderText(/text/i);
      const addButton = screen.getByText('Add Step');
      
      await user.type(actionValueInput, 'First');
      await user.click(addButton);
      
      await user.type(actionValueInput, 'Second');
      await user.click(addButton);
      
      await user.type(actionValueInput, 'Third');
      await user.click(addButton);
      
      // Check step numbers
      expect(screen.getByText('1.')).toBeInTheDocument();
      expect(screen.getByText('2.')).toBeInTheDocument();
      expect(screen.getByText('3.')).toBeInTheDocument();
    });
  });

  describe('Action Type Switching', () => {
    it('should update placeholder text when action type changes', async () => {
      renderDialog();
      
      await user.click(screen.getByRole('button', { name: 'Create Plan' }));
      
      const actionTypeSelect = screen.getByLabelText(/Action Type/i);
      
      // Check speech placeholder
      await user.selectOptions(actionTypeSelect, 'speech');
      // The placeholder might be set dynamically, so we need to check the input
      const speechInput = screen.getByPlaceholderText(/text/i);
      expect(speechInput).toBeInTheDocument();
      
      // Check gesture placeholder
      await user.selectOptions(actionTypeSelect, 'gesture');
      const gestureInput = screen.getByTestId("valueEditorTestID")
      expect(gestureInput).toBeInTheDocument();
      
      // Check LLM placeholder
      await user.selectOptions(actionTypeSelect, 'llm');
      const llmInput = screen.getByPlaceholderText(/LLM|text/i);
      expect(llmInput).toBeInTheDocument();
    });
  });

  describe('Plan reducing', () => {
    it('should correctly reduce the plan given the elements of the plan', () => {
      // Create a plan for testing
      const testplan = extendedPlan
      const mockGoalNode: Node<GoalNodeData> = {
            id: 'goal-1',
            type: 'goal',
            position: { x: 0, y: 0 },
            data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'mock goal', plan: defaultPlan },
          };

      // Insert the goal and retrieve its expected data
      const newTestPlan = insertGoalInPlan(testplan, mockGoalNode)
      const goalReduced = GoalReduce(mockGoalNode, [mockGoalNode])
      const expectedResult = {
        id: "extended-plan-1",
        steps: [
          {
            id: "firststep",
            gesture: {
              type: "tag",
              name: "hello"
            }
          },
          {
            id: "secondstep",
            gesture: {
              type: "single",
              name: "somefolder/somegesture"
            }
          },
          {
            id: "thirdstep",
            goal: "ask the user something or whatever"
          },
          {
            id: "fourthstep",
            text: "I'm a cyborg ninja :>"
          },
            goalReduced,
        ]
      }
      
      // Check to see it the goal got added, and its reduced data was added to the goals'
      const actualResult = PlanReduce([mockGoalNode], newTestPlan) 
      expect(actualResult).toEqual(expectedResult)
    });
  })
});