/*
 * 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.nifi.controller;

import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.reporting.InitializationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TestControllerServiceApiMatcher {

    private ControllerServiceApiMatcher serviceApiMatcher;

    @BeforeEach
    public void setup() {
        this.serviceApiMatcher = new ControllerServiceApiMatcher();
    }

    @Test
    public void testServiceImplementationMatchesRealServiceApi() {
        assertTrue(serviceApiMatcher.matches(FooServiceApiV1.class, FooServiceImplementationV1.class));
    }

    @Test
    public void testServiceImplementationMatchesCompatibleServiceApi() {
        // v2 implementation should match the real v2 API that it implements
        assertTrue(serviceApiMatcher.matches(FooServiceApiV2.class, FooServiceImplementationV2.class));
        // v2 implementation should also match the v1 API since the methods are the same
        assertTrue(serviceApiMatcher.matches(FooServiceApiV1.class, FooServiceImplementationV2.class));
    }

    @Test
    public void testServiceImplementationDoesNotMatchIncompatibleApi() {
        assertFalse(serviceApiMatcher.matches(FooServiceApiV3.class, FooServiceImplementationV1.class));
        assertFalse(serviceApiMatcher.matches(FooServiceApiV3.class, FooServiceImplementationV2.class));
    }

    @Test
    public void testServiceApiWithGenerics() {
        // should match
        assertTrue(serviceApiMatcher.matches(GenericServiceApiV1.class, GenericServiceImpl.class));
        // should not match because method changed args
        assertFalse(serviceApiMatcher.matches(GenericServiceApiV2.class, GenericServiceImpl.class));
        // should not match because method changed return type
        assertFalse(serviceApiMatcher.matches(GenericServiceApiV3.class, GenericServiceImpl.class));
    }

    // Interface for a result
    private interface FooResult {
        String getResult();
    }

    // Implementation for a result
    private class FooResultImpl implements FooResult {
        private final String result;

        public FooResultImpl(final String result) {
            this.result = result;
        }

        @Override
        public String getResult() {
            return result;
        }
    }

    // Interface for an argument
    private interface FooArg {
        String getArg();
    }

    // Implementation for an argument
    private static class FooArgImpl implements FooArg {
        private final String arg;

        public FooArgImpl(final String arg) {
            this.arg = arg;
        }

        @Override
        public String getArg() {
            return arg;
        }
    }

    // Service API v1
    private interface FooServiceApiV1 extends ControllerService {
        void execute();
        String execute(String a);
        FooResult executeWithReturn(FooArg a);
    }

    // Service API v2, unchanged from v1
    private interface FooServiceApiV2 extends ControllerService {
        void execute();
        String execute(String a);
        FooResult executeWithReturn(FooArg a);
    }

    // Service API v3, new method added since v1/v3
    private interface FooServiceApiV3 extends ControllerService {
        void execute();
        String execute(String a);
        FooResult executeWithReturn(FooArg a);
        String someNewMethod();
    }

    // Service implementing v1 API
    private static class FooServiceImplementationV1 implements FooServiceApiV1 {

        @Override
        public void execute() {

        }

        @Override
        public String execute(String a) {
            return null;
        }

        // Declare the impl result here to test comparing a more specific return type against the API
        @Override
        public FooResultImpl executeWithReturn(FooArg a) {
            return null;
        }

        @Override
        public void initialize(ControllerServiceInitializationContext context) throws InitializationException {

        }

        @Override
        public Collection<ValidationResult> validate(ValidationContext context) {
            return null;
        }

        @Override
        public PropertyDescriptor getPropertyDescriptor(String name) {
            return null;
        }

        @Override
        public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {

        }

        @Override
        public List<PropertyDescriptor> getPropertyDescriptors() {
            return null;
        }

        @Override
        public String getIdentifier() {
            return null;
        }
    }

    // Service implementing v2 API
    private static class FooServiceImplementationV2 implements FooServiceApiV2 {

        @Override
        public void execute() {

        }

        @Override
        public String execute(String a) {
            return null;
        }

        @Override
        public FooResult executeWithReturn(FooArg a) {
            return null;
        }

        @Override
        public void initialize(ControllerServiceInitializationContext context) throws InitializationException {

        }

        @Override
        public Collection<ValidationResult> validate(ValidationContext context) {
            return null;
        }

        @Override
        public PropertyDescriptor getPropertyDescriptor(String name) {
            return null;
        }

        @Override
        public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {

        }

        @Override
        public List<PropertyDescriptor> getPropertyDescriptors() {
            return null;
        }

        @Override
        public String getIdentifier() {
            return null;
        }
    }

    // Service API with generics v1
    private interface GenericServiceApiV1<T extends FooResult, V extends FooArg> extends ControllerService {
        T execute(V arg);
    }

    // Service API with generics v2
    private interface GenericServiceApiV2<T extends FooResult, V extends FooArg> extends ControllerService {
        T execute(V arg1, V arg2);
    }

    // Service API with generics v3
    private interface GenericServiceApiV3<V extends FooArg> extends ControllerService {
        String execute(V arg);
    }

    // Service implementation with generics
    private static class GenericServiceImpl implements GenericServiceApiV1<FooResultImpl,FooArgImpl> {

        @Override
        public FooResultImpl execute(FooArgImpl arg) {
            return null;
        }

        @Override
        public void initialize(ControllerServiceInitializationContext context) throws InitializationException {

        }

        @Override
        public Collection<ValidationResult> validate(ValidationContext context) {
            return null;
        }

        @Override
        public PropertyDescriptor getPropertyDescriptor(String name) {
            return null;
        }

        @Override
        public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {

        }

        @Override
        public List<PropertyDescriptor> getPropertyDescriptors() {
            return null;
        }

        @Override
        public String getIdentifier() {
            return null;
        }
    }

}
