package org.testcontainers;

import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * This test checks that we don't expose any shaded class in our public API.
 * We use {@link Parameterized} runner here to create a test per public class in Testcontainers' JAR file.
 */
@RunWith(Parameterized.class)
@RequiredArgsConstructor
public class PublicBinaryAPITest extends AbstractJarFileTest {

    private static String SHADED_PACKAGE = "org.testcontainers.shaded.";
    private static String SHADED_PACKAGE_PATH = SHADED_PACKAGE.replaceAll("\\.", "/");

    static {
        Assertions.registerFormatterForType(ClassNode.class, it -> it.name);
        Assertions.registerFormatterForType(FieldNode.class, it -> it.name);
        Assertions.registerFormatterForType(MethodNode.class, it -> it.name + it.desc);
    }

    @Parameters(name = "{0}")
    public static List<Object[]> data() throws Exception {
        List<Object[]> result = new ArrayList<>();

        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
                String fileName = path.toString();

                if (!fileName.endsWith(".class")) {
                    return super.visitFile(path, attrs);
                }

                if (!fileName.startsWith("/org/testcontainers/") ) {
                     return super.visitFile(path, attrs);
                }

                if (fileName.startsWith("/" + SHADED_PACKAGE_PATH)) {
                    return super.visitFile(path, attrs);
                }

                try(InputStream inputStream = Files.newInputStream(path)) {
                    ClassReader reader = new ClassReader(inputStream);
                    ClassNode node = new ClassNode();
                    reader.accept(node, ClassReader.SKIP_CODE);
                    if ((node.access & Opcodes.ACC_PUBLIC) != 0) {
                        result.add(new Object[]{ fileName, node });
                    }
                }

                return super.visitFile(path, attrs);
            }
        });
        return result;
    }

    private final String fileName;

    private final ClassNode classNode;

    @Before
    public void setUp() {
        Assume.assumeFalse(classNode.name.startsWith("com.github.dockerjava."));
        switch (classNode.name) {
            // TODO should go to docker-java project
            case "org/testcontainers/dockerclient/auth/AuthDelegatingDockerClientConfig":
                Assume.assumeTrue(false);
        }
    }

    @Test
    public void testSuperClass() {
        assertThat(classNode.superName)
                .doesNotStartWith(SHADED_PACKAGE_PATH);
    }

    @Test
    public void testInterfaces() {
        assertThat(classNode.interfaces)
                .allSatisfy(it -> assertThat(it).doesNotStartWith(SHADED_PACKAGE_PATH));
    }

    @Test
    public void testMethodReturnTypes() {
        assertThat(classNode.methods)
                .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0)
                .allSatisfy(it -> assertThat(Type.getReturnType(it.desc).getClassName()).doesNotStartWith(SHADED_PACKAGE));
    }

    @Test
    public void testMethodArguments() {
        assertThat(classNode.methods)
                .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0)
                .allSatisfy(method -> assertThat(Arrays.asList(Type.getArgumentTypes(method.desc)))
                        .extracting(Type::getClassName)
                        .allSatisfy(it -> assertThat(it).doesNotStartWith(SHADED_PACKAGE))
                );
    }

    @Test
    public void testFields() {
        assertThat(classNode.fields)
                .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0)
                .allSatisfy(it -> assertThat(Type.getType(it.desc).getClassName())
                        .doesNotStartWith(SHADED_PACKAGE)
                );
    }
}