/*
 * Copyright 2023 JBoss by Red Hat.
 *
 * Licensed 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.jboss.as.test.manualmode.insights;

import com.fasterxml.jackson.databind.JsonNode;
import jakarta.inject.Inject;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.as.controller.client.helpers.standalone.ServerDeploymentHelper;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.core.testrunner.ServerControl;
import org.wildfly.core.testrunner.ServerController;
import org.wildfly.core.testrunner.WildFlyRunner;

import java.nio.file.Path;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

@RunWith(WildFlyRunner.class)
@RunAsClient
@ServerControl(manual = true)
public abstract class AbstractInsightsClientTestCase {

    // I4ASR0019 - all clients failed
    protected static final String INSIGHTS_DEBUG_ALL_CLIENTS_FAILED = ".*DEBUG \\[org.jboss.eap.insights.report\\].*I4ASR0019.*InsightsJdkHttpClient.*InsightsFileWritingClient.*";
    protected static final String INSIGHTS_IWE_PATTERN = ".*(INFO|WARN|ERROR) \\[org.jboss.eap.insights.report\\].*";

    private static final String OPT_OUT_PROPERTY = "rht.insights.java.opt.out";
    private static final String CONNECT_PERIOD_PROPERTY = "rht.insights.java.connect.period";
    private static final String UPDATE_PERIOD_PROPERTY = "rht.insights.java.update.period";

    private static final String MODULES_JAR = "jboss-modules.jar";
    private static final String REPORT_VERSION_FORMAT_PATTERN = "\\d+\\.\\d+\\.\\d+";

    @Inject
    protected static ServerController container;

    protected static InsightsClientPropertiesSetup setupTask = new InsightsClientPropertiesSetup();

    protected static Path serverLogFile;

    @Test
    public void testOptOut() throws Exception {
        container.startInAdminMode();
        try {
            setupTask.addProperty(container.getClient().getControllerClient(), OPT_OUT_PROPERTY, "true");
            container.stop();
            container.start();
            assertInsightsDisabled();
        } finally {
            setupTask.removeProperty(container.getClient().getControllerClient(), OPT_OUT_PROPERTY);
            container.stop();
        }
    }

    @Test
    public void testAdminOnly() throws Exception {
        assertReady();
        container.startInAdminMode();
        assertInsightsDisabled();
        container.stop();
    }

    @Test
    public void testConnect() throws Exception {
        assertReady();
        container.start();

        awaitConnect(20);
        InsightsRequest request = getConnect();
        assertTrue(request.isConnect());

        // these are generated by the runtimes client, so just some quick checks
        assertReportVersion(request.getPayload(), "top-level");
        assertNotNull("No ID hash", request.getPayload().get("idHash"));

        JsonNode basicReport = request.getPayload().get("basic");
        assertNotNull("No basic report", basicReport);
        assertTrue(basicReport.get("app.name").asText().startsWith("Red Hat JBoss EAP"));
        // check client implementation specifics of the basic report as these might come from the integration code
        checkBasicReportClientSpecifics(basicReport);

        JsonNode jarsReport = request.getPayload().get("jars");
        assertNotNull("No jars report", jarsReport);
        assertReportVersion(jarsReport, "jars");
        JsonNode jarsArray = jarsReport.get("jars");
        assertEquals(1, jarsArray.size());
        assertEquals(MODULES_JAR, jarsArray.get(0).get("name").asText());

        // EAP specific sub-reports start here
        JsonNode eapReport = request.getPayload().get("eap");
        assertNotNull("No eap report", eapReport);
        assertEquals("Incorrect product name in the EAP sub-report.", "Red Hat JBoss EAP", eapReport.get("name").asText());
        assertReportVersion(eapReport, "eap");

        assertFieldDefined(eapReport, "EAP", "eap-version");
        assertFieldPresent(eapReport, "EAP", "eap-installation");
        JsonNode eapInstallation = eapReport.get("eap-installation");
        assertReportVersion(eapInstallation, "eap-installation");
        assertFieldDefined(eapInstallation, "eap-installation", "eap-xp");
        assertFieldDefined(eapInstallation, "eap-installation", "yaml-extension");
        assertFieldDefined(eapInstallation, "eap-installation", "bootable-jar");

        JsonNode eapModules = eapReport.get("eap-modules");
        assertNotNull("No eap-modules sub-report", eapModules);
        assertReportVersion(eapModules, "eap-modules");
        assertFieldPresent(eapModules, "eap-modules", "jars");
        assertTrue("Jars array in the eap-modules report is empty.", eapModules.get("jars").size() > 0);

        JsonNode eapConfig = eapReport.get("eap-configuration");
        assertNotNull("No eap-configuration sub-report", eapConfig);
        assertReportVersion(eapConfig, "eap-configuration");
        assertFieldPresent(eapConfig, "eap-configuration", "configuration");

        JsonNode eapDeployments = eapReport.get("eap-deployments");
        assertNotNull("No eap-deployments sub-report", eapDeployments);
        assertReportVersion(eapReport, "eap-deployments");
        assertEquals(0, eapDeployments.get("deployments").size());

        container.stop();
    }

    @Test
    public void testConnectResendPeriod() throws Exception {
        assertReady();
        container.startInAdminMode();
        setupTask.addProperty(container.getClient().getControllerClient(), CONNECT_PERIOD_PROPERTY, "PT5S");
        container.stop();

        String deploymentName = "dummy.jar";
        try {
            container.start();
            awaitConnect(20);
            InsightsRequest firstConnect = getConnect();
            cleanRequests();
            awaitConnect(20);
            InsightsRequest secondConnect = getConnect();
            assertEquals(firstConnect.getPayload().get("basic").get("jvm.pid"), secondConnect.getPayload().get("basic").get("jvm.pid"));
            assertNotEquals(firstConnect.getPayload().get("basic").get("jvm.report_time"), secondConnect.getPayload().get("basic").get("jvm.report_time"));
            assertEquals(firstConnect.getPayload().get("eap").get("eap-modules"), secondConnect.getPayload().get("eap").get("eap-modules"));
            assertEquals(firstConnect.getPayload().get("eap").get("eap-deployments"), secondConnect.getPayload().get("eap").get("eap-deployments"));

            container.deploy(getJarDeployment(deploymentName), deploymentName);
            // clean requests twice here as it might happen that report gets generated at the time of cleaning and might not contain deployment yettestNoBinary
            cleanRequests();
            awaitConnect(10);
            cleanRequests();
            awaitConnect(10);
            InsightsRequest thirdConnect = getConnect();
            assertEquals(deploymentName, thirdConnect.getPayload().get("eap").get("eap-deployments").get("deployments").get(0).get("name").asText());
        } finally {
            setupTask.removeProperty(container.getClient().getControllerClient(), CONNECT_PERIOD_PROPERTY);
            undeploy(deploymentName);
            container.stop();
        }
    }

    @Test
    public void testUpdateDeploy() throws Exception {
        assertReady();
        container.startInAdminMode();
        setupTask.addProperty(container.getClient().getControllerClient(), UPDATE_PERIOD_PROPERTY, "PT2S");
        container.stop();

        String deploymentName = "dummy.jar";
        try {
            container.start();
            awaitConnect(20);
            InsightsRequest connectReq = getConnect();
            assertNotNull(connectReq);

            container.deploy(getJarDeployment(deploymentName), deploymentName);
            awaitUpdate(20);
            InsightsRequest updateRequest = getUpdate();
            assertTrue(updateRequest.isUpdate());
            assertReportVersion(updateRequest.getPayload(), "top-level");
            assertEquals("The update request has a different ID", connectReq.getPayload().get("idHash").asText(), updateRequest.getPayload().get("idHash").asText());
            JsonNode updatedJars = updateRequest.getPayload().get("updated-jars");
            assertReportVersion(updatedJars, "top-level");
            assertFieldPresent(updatedJars, "updated-jars", "jars");
            assertEquals("Wrong size of jars array in update report.", 1, updatedJars.get("jars").size());
            JsonNode dummyJarNode = updatedJars.get("jars").get(0);
            assertFieldDefined(dummyJarNode, "dummy deployment node", "name");
            assertEquals("Wrong deplyment name in update report.", deploymentName, dummyJarNode.get("name").asText());
            assertFieldPresent(dummyJarNode, "dummy deployment node", "version");
            assertFieldPresent(dummyJarNode, "dummy deployment node", "attributes");
            JsonNode attributesNode = dummyJarNode.get("attributes");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "sha1Checksum");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "path");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "sha256Checksum");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "sha512Checksum");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "runtime-name");
            assertFieldDefined(attributesNode, "dummy deployment attributes", "deployment");

            cleanRequests();
            container.deploy(getJarDeployment("2" + deploymentName), "2" + deploymentName);
            awaitUpdate(20);
            updateRequest = getUpdate();
            assertEquals("Wrong deplyment name in update report.", "2" + deploymentName, updateRequest.getPayload().get("updated-jars").get("jars").get(0).get("name").asText());
        } finally {
            setupTask.removeProperty(container.getClient().getControllerClient(), UPDATE_PERIOD_PROPERTY);
            undeploy(deploymentName);
            undeploy("2" + deploymentName);
            container.stop();
        }
    }

    private void assertInsightsDisabled() throws Exception {
        Thread.sleep(20000);
        assertNoRequest();
    }

    protected void assertFieldPresent(JsonNode report, String reportName, String fieldName) {
        assertNotNull(String.format("Missing %s field in the %s sub-report.", fieldName, reportName), report.get(fieldName));
    }

    protected void assertFieldDefined(JsonNode report, String reportName, String fieldName) {
        assertFieldPresent(report, reportName, fieldName);
        assertFalse(String.format("Empty %s field in the %s sub-report.", fieldName, reportName), report.get(fieldName).asText().isEmpty());
    }

    private void assertReportVersion(JsonNode report, String reportName) {
        assertNotNull("Missing report version in the report " + reportName, report.get("version"));
        assertTrue("Wrong report version format in the report " + reportName, report.get("version").asText().matches(REPORT_VERSION_FORMAT_PATTERN));
    }

    private JavaArchive getJarDeployment(String deploymentName) {
        return ShrinkWrap.create(JavaArchive.class, deploymentName)
                .add(new StringAsset("foo"), "foo.txt");
    }

    private void undeploy(String deploymentName) throws Exception {
        try {
            container.undeploy(deploymentName);
        } catch (ServerDeploymentHelper.ServerDeploymentException ex) {
            // ignore deployment not found
            if (!ex.getMessage().contains("WFLYCTL0216")) {
                throw ex;
            }
        }
    }

    protected abstract void assertNoRequest() throws Exception;

    protected abstract void assertReady() throws Exception;

    protected abstract void awaitConnect(long seconds) throws Exception;

    protected abstract void awaitUpdate(long seconds) throws Exception;

    protected abstract InsightsRequest getConnect() throws Exception;

    protected abstract InsightsRequest getUpdate() throws Exception;

    protected abstract void cleanRequests() throws Exception;

    protected abstract void checkBasicReportClientSpecifics(JsonNode basicReport);
}
