package de.renew.engine.simulator;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;

import de.renew.engine.searcher.EarlyExecutable;
import de.renew.engine.searcher.Executable;
import de.renew.engine.searcher.LateExecutable;
import de.renew.engine.searcher.Occurrence;
import de.renew.engine.searcher.OccurrenceDescription;
import de.renew.engine.searcher.Searcher;
import de.renew.engine.searcher.VariableMapperCopier;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.StepIdentifier;
import de.renew.unify.Impossible;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for the {@link Binding} class.
 * <p>
 * Verifies construction, execution order, description generation, error handling,
 * and behavior with various executable scenarios.
 * <p>
 * Mocks dependencies
 */
public class BindingTest {

    private static final String DESCRIPTION_1 = "Test Description 1";
    private static final String DESCRIPTION_2 = "Test Description 2";

    private MockedStatic<SimulationThreadPool> _simulationThreadPoolMockedStatic;
    private AutoCloseable _closeable;

    private Binding _binding;

    @Mock
    private Searcher _mockSearcher;

    @Mock
    private Occurrence _mockOccurrence1;

    @Mock
    private Occurrence _mockOccurrence2;

    @Mock
    private EarlyExecutable _mockEarlyExecutable1;

    @Mock
    private EarlyExecutable _mockEarlyExecutable2;

    @Mock
    private LateExecutable _mockLateExecutable1;

    @Mock
    private LateExecutable _mockLateExecutable2;

    @Mock
    private OccurrenceDescription _mockOccurrenceDescription1;

    @Mock
    private OccurrenceDescription _mockOccurrenceDescription2;

    @Mock
    private StepIdentifier _mockStepIdentifier;

    @Mock
    private SimulationThreadPool _mockThreadPool;

    /**
     * Sets up the test environment. Creates the occurrences and executables.
     */
    @BeforeEach
    public void setUp() {
        _closeable = MockitoAnnotations.openMocks(this);

        _simulationThreadPoolMockedStatic = mockStatic(SimulationThreadPool.class);
        _simulationThreadPoolMockedStatic.when(SimulationThreadPool::isSimulationThread)
            .thenReturn(true);
        _simulationThreadPoolMockedStatic.when(SimulationThreadPool::getCurrent)
            .thenReturn(_mockThreadPool);

        List<Occurrence> occurrences = Arrays.asList(_mockOccurrence1, _mockOccurrence2);
        when(_mockSearcher.getOccurrences()).thenReturn(occurrences);

        List<Executable> executables1 = Arrays.asList(_mockEarlyExecutable1, _mockLateExecutable1);
        when(_mockOccurrence1.makeExecutables(any(VariableMapperCopier.class)))
            .thenReturn(executables1);

        List<Executable> executables2 = Arrays.asList(_mockEarlyExecutable2, _mockLateExecutable2);
        when(_mockOccurrence2.makeExecutables(any(VariableMapperCopier.class)))
            .thenReturn(executables2);

        when(_mockOccurrence1.makeOccurrenceDescription(any(VariableMapperCopier.class)))
            .thenReturn(_mockOccurrenceDescription1);
        when(_mockOccurrence2.makeOccurrenceDescription(any(VariableMapperCopier.class)))
            .thenReturn(_mockOccurrenceDescription2);

        when(_mockOccurrenceDescription1.getDescription()).thenReturn(DESCRIPTION_1);
        when(_mockOccurrenceDescription2.getDescription()).thenReturn(DESCRIPTION_2);

        when(_mockLateExecutable1.isLong()).thenReturn(false);
        when(_mockLateExecutable2.isLong()).thenReturn(false);

        _binding = new Binding(_mockSearcher);

        doAnswer(invocation -> {
            _binding.run();
            return null;
        }).when(_mockThreadPool).executeAndWait(_binding);

        doAnswer(invocation -> {
            _binding.run();
            return null;
        }).when(_mockThreadPool).execute(_binding);

    }

    /**
     * Tears down the test environment by closing resources.
     *
     * @throws Exception if the closeable cannot be closed
     */
    @AfterEach
    public void tearDown() throws Exception {
        if (_simulationThreadPoolMockedStatic != null) {
            _simulationThreadPoolMockedStatic.close();
        }
        if (_closeable != null) {
            _closeable.close();
        }
    }

    /**
     * Verifies that the constructor initializes the binding correctly
     * by retrieving occurrences and their executables.
     */
    @Test
    public void testBindingConstructor() {
        // then
        verify(_mockSearcher).getOccurrences();
        verify(_mockOccurrence1).makeExecutables(any(VariableMapperCopier.class));
        verify(_mockOccurrence2).makeExecutables(any(VariableMapperCopier.class));
        verify(_mockOccurrence1).makeOccurrenceDescription(any(VariableMapperCopier.class));
        verify(_mockOccurrence2).makeOccurrenceDescription(any(VariableMapperCopier.class));
    }

    /**
     * Tests that {@link Binding#getDescription()} returns the correct
     * combined description from all occurrences.
     */
    @Test
    public void testGetDescription() {
        // when
        String description = _binding.getDescription();

        // then
        assertThat(description).isEqualTo(DESCRIPTION_1 + "\n" + DESCRIPTION_2);
        verify(_mockOccurrenceDescription1, times(1)).getDescription();
        verify(_mockOccurrenceDescription2, times(1)).getDescription();
    }

    /**
     * Verifies that {@link Binding#execute} runs synchronously/asynchronously and invokes
     * {@code execute()} or {@code executeAndWait()} on the simulation thread pool.
     *
     * @param async {@code true} if the execution is async
     */
    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    public void testExecute(boolean async) {
        // when
        boolean result = _binding.execute(_mockStepIdentifier, async);

        // then
        assertThat(result).isTrue();
        if (async) {
            verify(_mockThreadPool).execute(_binding);
        } else {
            verify(_mockThreadPool).executeAndWait(_binding);
        }
    }

    /**
     * Ensures executables are invoked in the correct order during execution.
     */
    @Test
    public void testExecuteOrderOfExecutables() throws Impossible {
        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isTrue();

        InOrder inOrder = inOrder(
            _mockEarlyExecutable1, _mockEarlyExecutable2, _mockLateExecutable1,
            _mockLateExecutable2);

        inOrder.verify(_mockEarlyExecutable1).lock();
        inOrder.verify(_mockEarlyExecutable2).lock();
        inOrder.verify(_mockEarlyExecutable1).verify(_mockStepIdentifier);
        inOrder.verify(_mockEarlyExecutable2).verify(_mockStepIdentifier);
        inOrder.verify(_mockEarlyExecutable1).execute(_mockStepIdentifier);
        inOrder.verify(_mockEarlyExecutable2).execute(_mockStepIdentifier);
        inOrder.verify(_mockEarlyExecutable1).unlock();
        inOrder.verify(_mockEarlyExecutable2).unlock();
        inOrder.verify(_mockLateExecutable1).execute(_mockStepIdentifier);
        inOrder.verify(_mockLateExecutable2).execute(_mockStepIdentifier);
    }

    /**
     * Verifies that if verification fails immediately, no executables are executed,
     * and no rollback is triggered.
     */
    @Test
    public void testExecuteFailureDuringVerification() throws Impossible {
        // given
        doThrow(new Impossible(":(")).when(_mockEarlyExecutable1).verify(_mockStepIdentifier);

        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isFalse();

        verify(_mockEarlyExecutable1).lock();
        verify(_mockEarlyExecutable2).lock();
        verify(_mockEarlyExecutable1).unlock();
        verify(_mockEarlyExecutable2).unlock();
        verify(_mockEarlyExecutable1).verify(_mockStepIdentifier);
        verify(_mockEarlyExecutable1, never()).rollback();
        verify(_mockEarlyExecutable2, never()).rollback();
        verify(_mockEarlyExecutable1, never()).execute(_mockStepIdentifier);
        verify(_mockEarlyExecutable2, never()).execute(_mockStepIdentifier);
        verify(_mockLateExecutable1, never()).execute(_mockStepIdentifier);
        verify(_mockLateExecutable2, never()).execute(_mockStepIdentifier);
    }

    /**
     * Verifies that if verification fails after a successful verification,
     * the already verified executable is rolled back.
     */
    @Test
    public void testExecuteFailureAfterPartialVerification() throws Impossible {
        // given
        Occurrence mockOccurrence3 = mock(Occurrence.class);
        List<Occurrence> occurrences =
            Arrays.asList(_mockOccurrence1, mockOccurrence3, _mockOccurrence2);
        when(_mockSearcher.getOccurrences()).thenReturn(occurrences);

        EarlyExecutable mockEarlyExecutable3 = mock(EarlyExecutable.class);
        List<Executable> executables3 = Collections.singletonList(mockEarlyExecutable3);
        when(mockOccurrence3.makeExecutables(any(VariableMapperCopier.class)))
            .thenReturn(executables3);

        _binding = new Binding(_mockSearcher);
        doAnswer(invocation -> {
            _binding.run();
            return null;
        }).when(_mockThreadPool).executeAndWait(_binding);

        doThrow(new Impossible(":(")).when(_mockEarlyExecutable2).verify(_mockStepIdentifier);

        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isFalse();

        InOrder inOrder =
            inOrder(_mockEarlyExecutable1, _mockEarlyExecutable2, mockEarlyExecutable3);

        inOrder.verify(_mockEarlyExecutable1).lock();
        inOrder.verify(mockEarlyExecutable3).lock();
        inOrder.verify(_mockEarlyExecutable2).lock();

        inOrder.verify(_mockEarlyExecutable1).verify(_mockStepIdentifier);
        inOrder.verify(mockEarlyExecutable3).verify(_mockStepIdentifier);
        inOrder.verify(_mockEarlyExecutable2).verify(_mockStepIdentifier);

        inOrder.verify(mockEarlyExecutable3).rollback();
        inOrder.verify(_mockEarlyExecutable1).rollback();
        inOrder.verify(_mockEarlyExecutable2, never()).rollback();

        inOrder.verify(_mockEarlyExecutable1).unlock();
        inOrder.verify(mockEarlyExecutable3).unlock();
        inOrder.verify(_mockEarlyExecutable2).unlock();

        verify(_mockEarlyExecutable1, never()).execute(_mockStepIdentifier);
        verify(_mockEarlyExecutable2, never()).execute(_mockStepIdentifier);
        verify(_mockLateExecutable1, never()).execute(_mockStepIdentifier);
        verify(_mockLateExecutable2, never()).execute(_mockStepIdentifier);
    }

    /**
     * Ensures that if a {@link LateExecutable} throws an exception,
     * the next executable is called via {@link LateExecutable#executeAfterException}.
     */
    @Test
    public void testLateExecutableWithException() throws Impossible {
        // given
        RuntimeException testException = new RuntimeException(":(");
        doThrow(testException).when(_mockLateExecutable1).execute(_mockStepIdentifier);

        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isTrue();
        verify(_mockLateExecutable1).execute(_mockStepIdentifier);
        verify(_mockLateExecutable2).executeAfterException(_mockStepIdentifier, testException);
    }

    /**
     * Verifies that late executables marked as long are executed properly.
     */
    @Test
    public void testLongLateExecutable() throws Impossible {
        // given
        when(_mockLateExecutable1.isLong()).thenReturn(true);

        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isTrue();
        verify(_mockLateExecutable1).execute(_mockStepIdentifier);
        verify(_mockLateExecutable2).execute(_mockStepIdentifier);
    }

    /**
     * Verifies that calling {@link Binding#execute} twice on the same binding throws an exception.
     */
    @Test
    public void testExecuteDouble() {
        //given
        _binding.execute(_mockStepIdentifier, false);

        // when/then
        assertThatThrownBy(() -> _binding.execute(_mockStepIdentifier, false))
            .isInstanceOf(RuntimeException.class);
    }

    /**
     * Ensures an exception is thrown when an unknown executable type is encountered.
     */
    @Test
    public void testUnknownExecutableType() {
        // given
        Executable unknownExecutable = mock(Executable.class);
        List<Executable> unknownExecutables = Collections.singletonList(unknownExecutable);
        when(_mockOccurrence1.makeExecutables(any(VariableMapperCopier.class)))
            .thenReturn(unknownExecutables);

        // when/then
        assertThatThrownBy(() -> new Binding(_mockSearcher)).isInstanceOf(RuntimeException.class);
    }

    /**
     * Verifies that early executables are sorted correctly by lockPriority
     * during locking, and by phase during verification/execution.
     */
    @Test
    public void testExecuteRespectsLockAndPhaseOrdering() throws Exception {
        when(_mockEarlyExecutable1.lockPriority()).thenReturn(3L);
        when(_mockEarlyExecutable1.phase()).thenReturn(20);

        when(_mockEarlyExecutable2.lockPriority()).thenReturn(1L);
        when(_mockEarlyExecutable2.phase()).thenReturn(10);

        // when
        boolean result = _binding.execute(_mockStepIdentifier, false);

        // then
        assertThat(result).isTrue();

        InOrder lockOrder = inOrder(_mockEarlyExecutable2, _mockEarlyExecutable1);
        lockOrder.verify(_mockEarlyExecutable2).lock();
        lockOrder.verify(_mockEarlyExecutable1).lock();

        InOrder phaseOrder = inOrder(_mockEarlyExecutable2, _mockEarlyExecutable1);
        phaseOrder.verify(_mockEarlyExecutable2).verify(_mockStepIdentifier);
        phaseOrder.verify(_mockEarlyExecutable1).verify(_mockStepIdentifier);
        phaseOrder.verify(_mockEarlyExecutable2).execute(_mockStepIdentifier);
        phaseOrder.verify(_mockEarlyExecutable1).execute(_mockStepIdentifier);

        verify(_mockEarlyExecutable1).unlock();
        verify(_mockEarlyExecutable2).unlock();
    }
}