package com.github.containersolutions.operator;

import com.github.containersolutions.operator.sample.TestCustomResource;
import com.github.containersolutions.operator.sample.TestCustomResourceController;
import com.github.containersolutions.operator.sample.TestCustomResourceSpec;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition;
import io.fabric8.kubernetes.client.CustomResourceDoneable;
import io.fabric8.kubernetes.client.CustomResourceList;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;

import static com.github.containersolutions.operator.ControllerUtils.getCustomResourceDoneableClass;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

public class IntegrationTestSupport {

    public static final String TEST_NAMESPACE = "java-operator-sdk-int-test";
    public static final String TEST_CUSTOM_RESOURCE_PREFIX = "test-custom-resource-";
    private final static Logger log = LoggerFactory.getLogger(IntegrationTestSupport.class);
    private KubernetesClient k8sClient;
    private MixedOperation<TestCustomResource, CustomResourceList, CustomResourceDoneable,
            Resource<TestCustomResource, CustomResourceDoneable>> crOperations;
    private Operator operator;
    private TestCustomResourceController controller;

    public void initialize() {
        initialize(true);
    }

    public void initialize(boolean updateStatus) {
        k8sClient = new DefaultKubernetesClient();

        log.info("Initializing integration test in namespace {}", TEST_NAMESPACE);

        CustomResourceDefinition crd = loadYaml(CustomResourceDefinition.class, "test-crd.yaml");
        k8sClient.customResourceDefinitions().createOrReplace(crd);

        controller = new TestCustomResourceController(k8sClient, updateStatus);
        Class doneableClass = getCustomResourceDoneableClass(controller);
        crOperations = k8sClient.customResources(crd, TestCustomResource.class, CustomResourceList.class, doneableClass);
        crOperations.inNamespace(TEST_NAMESPACE).delete(crOperations.list().getItems());

        if (k8sClient.namespaces().withName(TEST_NAMESPACE).get() == null) {
            k8sClient.namespaces().create(new NamespaceBuilder()
                    .withMetadata(new ObjectMetaBuilder().withName(TEST_NAMESPACE).build()).build());
        }
        operator = new Operator(k8sClient);
        operator.registerController(controller, TEST_NAMESPACE);
        log.info("Operator is running with TestCustomeResourceController");
    }

    public void cleanup() {
        log.info("Cleaning up namespace {}", TEST_NAMESPACE);

        //we depend on the actual operator from the startup to handle the finalizers and clean up
        //resources from previous test runs

        await("all CRs cleaned up").atMost(60, TimeUnit.SECONDS)
                .untilAsserted(() -> {
                    assertThat(crOperations.inNamespace(TEST_NAMESPACE).list().getItems()).isEmpty();

                });

        k8sClient.configMaps().inNamespace(TEST_NAMESPACE)
                .withLabel("managedBy", TestCustomResourceController.class.getSimpleName())
                .delete();

        await("all config maps cleaned up").atMost(60, TimeUnit.SECONDS)
                .untilAsserted(() -> {
                    assertThat(k8sClient.configMaps().inNamespace(TEST_NAMESPACE)
                            .withLabel("managedBy", TestCustomResourceController.class.getSimpleName())
                            .list().getItems().isEmpty());
                });

        log.info("Cleaned up namespace " + TEST_NAMESPACE);
    }

    /**
     * Use this method to execute the cleanup of the integration test namespace only in case the test
     * was successful. This is useful to keep the Kubernetes resources around to debug a failed test run.
     * Unfortunately I couldn't make this work with standard JUnit methods as the @AfterAll method doesn't know
     * if the tests succeeded or not.
     *
     * @param test The code of the actual test.
     * @throws Exception if the test threw an exception.
     */
    public void teardownIfSuccess(TestRun test) {
        try {
            test.run();

            log.info("Deleting namespace {} and stopping operator", TEST_NAMESPACE);
            Namespace namespace = k8sClient.namespaces().withName(TEST_NAMESPACE).get();
            if (namespace.getStatus().getPhase().equals("Active")) {
                k8sClient.namespaces().withName(TEST_NAMESPACE).delete();
            }
            await("namespace deleted").atMost(30, TimeUnit.SECONDS)
                    .until(() -> k8sClient.namespaces().withName(TEST_NAMESPACE).get() == null);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            k8sClient.close();
        }
    }

    public int numberOfControllerExecutions() {
        return controller.getNumberOfExecutions();
    }

    private <T> T loadYaml(Class<T> clazz, String yaml) {
        try (InputStream is = getClass().getResourceAsStream(yaml)) {
            return Serialization.unmarshal(is, clazz);
        } catch (IOException ex) {
            throw new IllegalStateException("Cannot find yaml on classpath: " + yaml);
        }
    }

    public TestCustomResource createTestCustomResource(String id) {
        TestCustomResource resource = new TestCustomResource();
        resource.setMetadata(new ObjectMetaBuilder()
                .withName(TEST_CUSTOM_RESOURCE_PREFIX + id)
                .withNamespace(TEST_NAMESPACE)
                .build());
        resource.setKind("CustomService");
        resource.setSpec(new TestCustomResourceSpec());
        resource.getSpec().setConfigMapName("test-config-map-" + id);
        resource.getSpec().setKey("test-key");
        resource.getSpec().setValue(id);
        return resource;
    }

    public KubernetesClient getK8sClient() {
        return k8sClient;
    }

    public MixedOperation<TestCustomResource, CustomResourceList, CustomResourceDoneable, Resource<TestCustomResource, CustomResourceDoneable>> getCrOperations() {
        return crOperations;
    }

    public Operator getOperator() {
        return operator;
    }

    public interface TestRun {
        void run() throws Exception;
    }
}