/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 */
package org.apache.causeway.commons.internal.reflection;

import java.lang.reflect.Method;

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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;

import lombok.SneakyThrows;
import lombok.val;

class ClassCacheTest {

    static abstract class AbstractBase {
        void commonAction(){}
    }

    static class Concrete extends AbstractBase {
        void specificAction(){}
    }

    static class ConcreteOverride extends AbstractBase {
        @Override void commonAction(){}
        void specificAction(){}
    }

    static class Sample {
        String echoAction(final String x) {return x;}
    }

    private _ClassCache classCache;

    @BeforeEach
    void setup() {
        _ClassCache.invalidate();
        classCache = _ClassCache.getInstance();
    }

    @Test
    void weakMethodLookup() {
        assertNotNull(classCache.lookupResolvedMethodElseFail(Sample.class, "echoAction", new Class<?>[]{Object.class}));
    }
    @Test
    void exactMethodLookup() {
        assertNotNull(classCache.lookupResolvedMethodElseFail(Sample.class, "echoAction", new Class<?>[]{String.class}));
    }
    @Test
    void invalidMethodLookup() {
        assertThrows(NoSuchMethodException.class,
                ()->classCache.lookupResolvedMethodElseFail(Sample.class, "echoAction", new Class<?>[]{Integer.class}));
    }

    @Test
    void inhertitedMethodWhenUsingReflectionUtility() {
        val declaredMethods = Can.ofStream(
                _NullSafe.stream(_Reflect.streamAllMethods(Concrete.class, true)));
        assertContainsMethod(declaredMethods, "commonAction");
        assertContainsMethod(declaredMethods, "specificAction");
    }

    @Test
    void inhertitedMethod() {
        val declaredMethods = Can.ofStream(
                classCache.streamResolvedMethods(Concrete.class));
        assertContainsResolvedMethod(declaredMethods, "commonAction");
        assertContainsResolvedMethod(declaredMethods, "specificAction");
    }

    @Test
    void inhertitedMethodWhenOverride() {
        val declaredMethods = Can.ofStream(
                classCache.streamResolvedMethods(ConcreteOverride.class));
        assertContainsResolvedMethod(declaredMethods, "commonAction");
        assertContainsResolvedMethod(declaredMethods, "specificAction");
    }

    @ParameterizedTest(name = "{index}: {0}")
    @ValueSource(classes = {
            _Abstract.class,
            _AbstractImpl.class,
            _Interface.class,
            _InterfaceImpl.class,
            _GenericAbstract.class,
            _GenericAbstractImpl.class,
            _GenericInterface.class,
            _GenericInterfaceImpl.class,
            _Mixins.Task1.Mixin.class,
            _Mixins.Task2.Mixin.class,
    })
    void methodEnumeration(final Class<?> classUnderTest) {
        val declaredMethods = Can.ofStream(
                classCache.streamResolvedMethods(classUnderTest));

        val expectations = extractExpectations(classUnderTest);
        expectations.assertAll(declaredMethods);
    }

    @Test
    void javaLangObjectPublicMethodsAreIgnored() {
        val javaLangObjectPublicMethods = classCache.streamPublicMethods(Object.class)
                .collect(Can.toCan());
        assertEquals(Can.empty(), javaLangObjectPublicMethods);
    }

    // -- HELPER

    @SneakyThrows
    private _Expectations extractExpectations(final Class<?> classUnderTest) {
        final Class<?> classThatProvidesExpectations = classUnderTest.getSimpleName().equals("Mixin")
                ? classUnderTest.getEnclosingClass()
                : classUnderTest;
        return (_Expectations) classThatProvidesExpectations.getDeclaredMethod("expectations").invoke(null);
    }

    private void assertContainsResolvedMethod(final Can<ResolvedMethod> declaredMethods, final String methodName) {
        assertContainsMethod(declaredMethods.map(ResolvedMethod::method), methodName);
    }

    private void assertContainsMethod(final Can<Method> declaredMethods, final String methodName) {

        final long methodCount =
            declaredMethods.stream()
            .filter(m->m.getName().equals(methodName))
            // using filter over peek here, because peek is unreliable with 'count()' terminal
            .filter(m->{
                assertNotNull(m);
                return true;
            })
            .count();

        assertTrue(methodCount>0);
    }


}
