package de.uni_hamburg.fs;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

import collections.CollectionEnumeration;
import collections.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;


class FSNodeTest {

    @Test
    void testConstructorWithTypeAndMap() {
        // given
        Type mockType = mock(Type.class);
        Map mockMap = mock(Map.class);
        Name mockFeatureName = mock(Name.class);
        Node mockNode = mock(Node.class);
        CollectionEnumeration mockEnumeration = mock(CollectionEnumeration.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);

        when(mockMap.keys()).thenReturn(mockEnumeration);
        when(mockEnumeration.hasMoreElements()).thenReturn(true, false);
        when(mockEnumeration.nextElement()).thenReturn(mockFeatureName);
        when(mockMap.at(mockFeatureName)).thenReturn(mockNode);
        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        // when
        FSNode fsNode = new FSNode(mockType, mockMap);

        // then
        assertThat(fsNode.getType()).isSameAs(mockType);
        verify(mockType).appropFeatureNames();
        verify(mockMap).keys();
        verify(mockMap).at(mockFeatureName);
        verify(mockEnumeration, times(2)).hasMoreElements();
        verify(mockEnumeration).nextElement();
    }

    @Test
    void testConstructorWithType() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        // when
        FSNode fsNode = new FSNode(mockType);

        // then
        assertThat(fsNode.getType()).isSameAs(mockType);
        verify(mockType).appropFeatureNames();
    }

    @Test
    void testConstructorWithStringType() throws UnificationFailure {
        // given
        String typeStr = "testType";
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);

        try (MockedStatic<ConjunctiveType> mockedConjunctiveType =
            mockStatic(ConjunctiveType.class)) {
            mockedConjunctiveType.when(() -> ConjunctiveType.getType(typeStr)).thenReturn(mockType);
            when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

            // when
            FSNode fsNode = new FSNode(typeStr);

            // then
            assertThat(fsNode.getType()).isSameAs(mockType);
            verify(mockType).appropFeatureNames();
        }
    }

    @Test
    void testHasFeatureWhenExists() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);
        Node mockNode = mock(Node.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        FSNode fsNode = new FSNode(mockType);
        fsNode.setFeature(mockFeatureName, mockNode);

        // when
        boolean result = fsNode.hasFeature(mockFeatureName);

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

    @Test
    void testHasFeatureWhenNotExists() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        FSNode fsNode = new FSNode(mockType);

        // when
        boolean result = fsNode.hasFeature(mockFeatureName);

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

    @Test
    void testDeltaWhenFeatureExists() throws NoSuchFeatureException {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);
        Node mockNode = mock(Node.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        FSNode fsNode = new FSNode(mockType);
        fsNode.setFeature(mockFeatureName, mockNode);

        // when
        Node result = fsNode.delta(mockFeatureName);

        // then
        assertThat(result).isSameAs(mockNode);
    }

    @Test
    void testDeltaWhenFeatureIsApprop() throws NoSuchFeatureException {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);
        Type mockAppropType = mock(Type.class);
        Node mockNode = mock(Node.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);
        when(mockType.isApprop(mockFeatureName)).thenReturn(true);
        when(mockType.appropType(mockFeatureName)).thenReturn(mockAppropType);
        when(mockAppropType.newNode()).thenReturn(mockNode);

        FSNode fsNode = new FSNode(mockType);

        // when
        Node result = fsNode.delta(mockFeatureName);

        // then
        assertThat(result).isSameAs(mockNode);
    }

    @Test
    void testDeltaWhenFeatureDoesNotExist() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);
        when(mockType.isApprop(mockFeatureName)).thenReturn(false);

        FSNode fsNode = new FSNode(mockType);

        // when & then
        assertThatThrownBy(() -> fsNode.delta(mockFeatureName))
            .isInstanceOf(NoSuchFeatureException.class);

    }

    @Test
    void testSetFeature() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);
        Node mockNode = mock(Node.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        FSNode fsNode = new FSNode(mockType);

        // when
        fsNode.setFeature(mockFeatureName, mockNode);

        // then
        assertThat(fsNode.hasFeature(mockFeatureName)).isTrue();
    }

    @Test
    void testDuplicate() {
        // given
        Type mockType = mock(Type.class);
        CollectionEnumeration mockAppropNames = mock(CollectionEnumeration.class);
        Name mockFeatureName = mock(Name.class);
        Node mockNode = mock(Node.class);

        when(mockType.appropFeatureNames()).thenReturn(mockAppropNames);

        FSNode fsNode = new FSNode(mockType);
        fsNode.setFeature(mockFeatureName, mockNode);

        // when
        Node duplicatedNode = fsNode.duplicate();

        // then
        assertThat(duplicatedNode).isInstanceOf(FSNode.class).returns(mockType, Node::getType)
            .satisfies(node -> assertThat(node.hasFeature(mockFeatureName)).isTrue());

        assertThat(duplicatedNode.featureNames()).isNotNull();
    }
}