package de.renew.net.inscription.transition;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;

import de.renew.engine.searcher.Binder;
import de.renew.engine.searcher.BindingBadness;
import de.renew.engine.searcher.Executable;
import de.renew.engine.searcher.Searcher;
import de.renew.engine.searcher.VariableMapperCopier;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.expression.Expression;
import de.renew.expression.LocalVariable;
import de.renew.expression.VariableExpression;
import de.renew.expression.VariableMapper;
import de.renew.net.Net;
import de.renew.net.NetElementID;
import de.renew.net.NetInstance;
import de.renew.net.Transition;
import de.renew.net.inscription.TransitionInscription;
import de.renew.unify.Copier;
import de.renew.unify.Impossible;
import de.renew.unify.Unify;
import de.renew.unify.Variable;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * Test class for {@link ConditionalOccurrence}.
 * Tests only the relevant (non-trivial) methods.
 */
@ExtendWith(MockitoExtension.class)
class ConditionalOccurrenceTest {
    private static final String TRANSITION_NAME = "Transition";

    private MockedStatic<SimulationThreadPool> _mockedPool;

    @Mock
    private NetInstance _mockNetInstance;

    private ConditionalInscription _conditionalInscription;

    private VariableMapper _mapper;

    private Transition _transition;

    private Searcher _searcher;

    private VariableMapperCopier _copier;

    private ConditionalOccurrence _conditionalOccurrence;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    void setUp() {
        _mockedPool = mockStatic(SimulationThreadPool.class);
        when(SimulationThreadPool.isSimulationThread()).thenReturn(true);

        _transition = new Transition(new Net(), TRANSITION_NAME, new NetElementID());
        _mapper = new VariableMapper();
        _searcher = new Searcher();
        _copier = new VariableMapperCopier(new Copier());
        _conditionalInscription = new ConditionalInscription(
            mock(Expression.class), mock(TransitionInscription.class), _transition);
        _conditionalOccurrence = new ConditionalOccurrence(
            _conditionalInscription, _mapper, _mockNetInstance, _transition);
    }

    /**
     * Setup method to close this test environment.
     */
    @AfterEach
    void tearDown() {
        _mockedPool.close();
    }

    /**
     * Test method for {@link ConditionalOccurrence#makeBinders(Searcher)}.
     *
     * @throws Impossible when the binders cannot be set up, implying
     *  that the occurrence cannot occur.
     */
    @Test
    public void testMakeBinders() throws Impossible {
        //given
        Variable variable = new Variable(
            _conditionalInscription.getConditionExpression().startEvaluation(
                _mapper, _searcher.getStateRecorder(), _searcher.getCalculationChecker()),
            _searcher.getStateRecorder());

        //when
        Collection<Binder> binders = _conditionalOccurrence.makeBinders(_searcher);

        //then
        assertThat(binders).isNotNull();
        assertThat(binders).hasSize(1);
        assertThat(binders).containsExactly(_conditionalOccurrence);
        assertThat(variable).isNotNull();
        assertThat(variable).usingRecursiveComparison()
            .isEqualTo(_conditionalOccurrence.getConditionVariable());
    }

    /**
     * Test method for {@link ConditionalOccurrence#bindingBadness(Searcher)}.
     *
     * @param localVariable The local variable or null if an anonymous variable must be
     *  generated.
     * @throws Impossible if unification is not possible.
     */
    @ParameterizedTest
    @MethodSource("provideLocalVariable")
    public void testBindingBadness(LocalVariable localVariable) throws Impossible {
        //given
        ConditionalOccurrence occurrence = createConditionalOccurrence(localVariable);
        Unify.unify(_mapper.map(localVariable), new Object(), null);

        //when/then
        assertThat(occurrence.bindingBadness(_searcher)).isEqualTo(1);
    }

    /**
     * Test method for {@link ConditionalOccurrence#bindingBadness(Searcher)}, when the localVariable is null.
     *
     * @throws Impossible if unification is not possible or when the binders cannot be set up, implying
     *  that the occurrence cannot occur.
     */
    @Test
    public void testBindingBadnessNull() throws Impossible {
        //given
        ConditionalOccurrence occurrence = createConditionalOccurrence(null);

        //when/then
        assertThat(occurrence.bindingBadness(_searcher)).isEqualTo(BindingBadness.MAX);
    }

    /**
     * Test method for {@link ConditionalOccurrence#bindingBadness(Searcher)}, when Value is not a Value Object.
     */
    @Test
    public void testBindWhenValueIsNotValueObject() {
        //given
        Searcher mockSearcher = mock(Searcher.class);
        ConditionalOccurrence conditionalOccurrence = mock(ConditionalOccurrence.class);

        //when
        conditionalOccurrence.bind(mockSearcher);

        //then
        verifyNoInteractions(mockSearcher);
    }

    /**
     * Test method for {@link ConditionalOccurrence#makeExecutables(VariableMapperCopier)}, when the field wantToOccur is false.
     *
     * @param localVariable The local variable or null if an anonymous variable must be
     *  generated.
     * @throws Impossible when the binders cannot be set up, implying
     *  that the occurrence cannot occur.
     */
    @ParameterizedTest
    @MethodSource("provideLocalVariable")
    public void testMakeExecutablesWhenWantToOccurIsFalse(LocalVariable localVariable)
        throws Impossible
    {
        //given
        ConditionalOccurrence occurrence = createConditionalOccurrence(localVariable);
        Collection<Executable> executables;

        //when
        executables = occurrence.makeExecutables(_copier);

        //then
        assertThat(executables).isEmpty();
    }

    /**
     * Test method for {@link ConditionalOccurrence#makeExecutables(VariableMapperCopier)}, when the field wantToOccur is true.
     *
     * @param localVariable The local variable or null if an anonymous variable must be
     *  generated.
     * @throws Impossible when the binders cannot be set up, implying
     *  that the occurrence cannot occur.
     */
    @ParameterizedTest
    @MethodSource("provideLocalVariable")
    public void testMakeExecutablesWhenWantToOccurIsTrue(LocalVariable localVariable)
        throws Impossible, NoSuchFieldException, IllegalAccessException
    {
        //given
        ConditionalOccurrence occurrence = createConditionalOccurrence(localVariable);
        Collection<Executable> executables;

        Field wantToOccur = ConditionalOccurrence.class.getDeclaredField("_wantToOccur");
        wantToOccur.setAccessible(true);
        wantToOccur.set(occurrence, true);

        ConditionalOccurrence mockOccurrence = mock(ConditionalOccurrence.class);
        when(mockOccurrence.makeExecutables(any())).thenReturn(List.of(mock(Executable.class)));

        Field secondaryOccurrences =
            ConditionalOccurrence.class.getDeclaredField("_secondaryOccurrences");
        secondaryOccurrences.setAccessible(true);
        secondaryOccurrences.set(occurrence, List.of(mockOccurrence));

        //when
        executables = occurrence.makeExecutables(_copier);

        //then
        verify(mockOccurrence).makeExecutables(_copier);
        assertThat(executables).isNotEmpty();
    }

    /**
     * Test method for {@link ConditionalOccurrence#makeOccurrenceDescription(VariableMapperCopier)}.
     *
     * @param localVariable The local variable or null if an anonymous variable must be
     *  generated.
     * @throws Impossible when the binders cannot be set up, implying
     *  that the occurrence cannot occur.
     */
    @ParameterizedTest
    @MethodSource("provideLocalVariable")
    public void testMakeOccurrenceDescription(LocalVariable localVariable) throws Impossible {
        //given
        ConditionalOccurrence occurrence = createConditionalOccurrence(localVariable);

        //when/then
        assertThat(occurrence.makeOccurrenceDescription(_copier)).isNull();
    }

    static Stream<Arguments> provideLocalVariable() {
        return Stream.of(
            Arguments.arguments(new LocalVariable(TRANSITION_NAME, true)),
            Arguments.arguments(new LocalVariable(TRANSITION_NAME, false)));
    }

    private ConditionalOccurrence createConditionalOccurrence(LocalVariable localVariable)
        throws Impossible
    {
        VariableExpression expression = new VariableExpression(Object.class, localVariable);
        ConditionalInscription inscription =
            new ConditionalInscription(expression, mock(TransitionInscription.class), _transition);
        ConditionalOccurrence occurrence =
            new ConditionalOccurrence(inscription, _mapper, _mockNetInstance, _transition);
        occurrence.makeBinders(_searcher);
        return occurrence;
    }
}