import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.lang.reflect.GenericArrayType;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class ContrattiReflection {
    public interface Catalogo {
        String cerca(String titolo);
    }

    public static class Servizio implements Catalogo {
        public String cerca(String titolo) {
            if (titolo.isBlank()) throw new IllegalArgumentException("Titolo vuoto");
            return "Trovato: " + titolo;
        }
        public int pagine(int numero) { return numero; }
        public int conta(String... titoli) { return titoli.length; }
    }

    static class Schema<T extends Number> {
        List<? extends Number> valori;
        T singolo;
        T[] serie;
    }

    static void verifica(boolean condizione) {
        if (!condizione) throw new AssertionError("Contratto non rispettato");
    }

    public static void main(String[] args) throws ReflectiveOperationException {
        Servizio servizio = new Servizio();
        var pagine = Servizio.class.getMethod("pagine", int.class);
        verifica(pagine.invoke(servizio, Integer.valueOf(7)).equals(7));
        try {
            Servizio.class.getMethod("pagine", Integer.class);
            throw new AssertionError("La ricerca non deve applicare boxing");
        } catch (NoSuchMethodException attesa) {
            System.out.println("Firma esatta: OK");
        }
        var conta = Servizio.class.getMethod("conta", String[].class);
        verifica(conta.invoke(servizio, (Object) new String[]{"Java", "I/O"}).equals(2));
        var lista = (ParameterizedType) Schema.class.getDeclaredField("valori").getGenericType();
        var limite = (WildcardType) lista.getActualTypeArguments()[0];
        verifica(limite.getUpperBounds()[0] == Number.class);
        verifica(Schema.class.getDeclaredField("singolo").getGenericType() instanceof TypeVariable<?>);
        verifica(Schema.class.getDeclaredField("serie").getGenericType() instanceof GenericArrayType);
        Object array = Array.newInstance(int.class, 3);
        Array.set(array, 1, 9);
        verifica(Array.getLength(array) == 3 && Array.get(array, 1).equals(9));
        try {
            Array.set(array, 0, "errore");
            throw new AssertionError("Array di tipo errato accettato");
        } catch (IllegalArgumentException attesa) {
            System.out.println("Tipi generici e array: OK");
        }
        AtomicInteger richieste = new AtomicInteger();
        Catalogo proxy = (Catalogo) Proxy.newProxyInstance(
                Catalogo.class.getClassLoader(), new Class<?>[]{Catalogo.class},
                (oggetto, metodo, argomenti) -> {
                    if (metodo.getDeclaringClass() == Object.class) {
                        return switch (metodo.getName()) {
                            case "equals" -> oggetto == argomenti[0];
                            case "hashCode" -> System.identityHashCode(oggetto);
                            case "toString" -> "Proxy catalogo";
                            default -> throw new AssertionError(metodo);
                        };
                    }
                    richieste.incrementAndGet();
                    try {
                        return metodo.invoke(servizio, argomenti);
                    } catch (InvocationTargetException ex) {
                        throw ex.getCause();
                    }
                });
        verifica(proxy.cerca("Java").equals("Trovato: Java"));
        verifica(proxy.equals(proxy) && proxy.toString().equals("Proxy catalogo"));
        try {
            proxy.cerca("");
            throw new AssertionError("Errore di dominio atteso");
        } catch (IllegalArgumentException attesa) {
            verifica(attesa.getMessage().equals("Titolo vuoto"));
        }
        verifica(richieste.get() == 2);
        System.out.println("Proxy e causa originale: OK");
    }
}
