// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.application;

import com.yahoo.component.Version;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

import static com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devAwsUsEast2a;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsCentral1;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testUsCentral1;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * @author jonmv
 * @author freva
 */
public class JobControllerApiHandlerHelperTest {

    @Test
    void testResponses() {
        ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
                .stagingTest()
                .blockChange(true, true, "mon,tue", "7-13", "UTC")
                .blockChange(false, true, "sun", "0-23", "CET")
                .blockChange(true, false, "fri-sat", "8", "America/Los_Angeles")
                .region("us-central-1")
                .test("us-central-1")
                .parallel("us-west-1", "us-east-3")
                .build();
        DeploymentTester tester = new DeploymentTester();
        var app = tester.newDeploymentContext();
        tester.clock().setInstant(Instant.EPOCH);

        // All completed runs will have a test report.
        tester.cloud().testReport(TestReport.fromJson("{\"summary\":{\"success\": 1, \"failed\": 0}}"));

        // Revision 1 gets deployed everywhere.
        app.submit(applicationPackage).deploy();
        RevisionId revision1 = app.lastSubmission().get();
        assertEquals(1000, tester.application().projectId().getAsLong());
        // System test includes test report
        assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), systemTest).get().id(), "0"), "system-test-log.json");

        tester.clock().advance(Duration.ofMillis(1000));
        // Revision 2 gets deployed everywhere except in us-east-3.
        RevisionId revision2 = app.submit(applicationPackage).lastSubmission().get();
        app.runJob(systemTest);
        app.runJob(stagingTest);
        app.runJob(productionUsCentral1);
        app.runJob(testUsCentral1);

        tester.triggerJobs();

        // us-east-3 eats the deployment failure and fails before deployment, while us-west-1 fails after.
        tester.configServer().throwOnNextPrepare(new ConfigServerException(INVALID_APPLICATION_PACKAGE, "ERROR!", "Failed to deploy application"));
        tester.runner().run();
        assertEquals(invalidApplication, tester.jobs().last(app.instanceId(), productionUsEast3).get().status());

        tester.runner().run();
        tester.clock().advance(Duration.ofHours(4).plusSeconds(1));
        tester.runner().run();
        assertEquals(installationFailed, tester.jobs().last(app.instanceId(), productionUsWest1).get().status());
        assertEquals(revision2, app.deployment(productionUsCentral1.zone()).revision());
        assertEquals(revision1, app.deployment(productionUsEast3.zone()).revision());
        assertEquals(revision2, app.deployment(productionUsWest1.zone()).revision());

        tester.clock().advance(Duration.ofMillis(1000));

        // Revision 3 starts.
        app.submit(applicationPackage)
                .runJob(systemTest).runJob(stagingTest);
        tester.triggerJobs(); // Starts runs for us-central-1 and a new staging test run.
        tester.runner().run();
        assertEquals(running, tester.jobs().last(app.instanceId(), productionUsCentral1).get().status());
        assertEquals(running, tester.jobs().last(app.instanceId(), stagingTest).get().status());

        // Staging deployment expires and the job fails, and is immediately retried.
        tester.controller().applications().deactivate(app.instanceId(), stagingTest.zone());
        tester.runner().run();
        assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());

        // Staging deployment expires again, the job fails for the second time, and won't be retried immediately.
        tester.clock().advance(Duration.ofMillis(100_000)); // Advance time to avoid immediate retry
        tester.triggerJobs();
        tester.runner().run();
        assertEquals(running, tester.jobs().last(app.instanceId(), stagingTest).get().status());
        tester.controller().applications().deactivate(app.instanceId(), stagingTest.zone());
        tester.runner().run();
        assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());

        tester.triggerJobs();
        assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());

        // System upgrades to a new version, which won't yet start.
        Version platform = new Version("7.1");
        tester.controllerTester().upgradeSystem(platform);
        tester.upgrader().maintain();
        tester.triggerJobs();

        // us-central-1 has started, deployed, and is installing. Deployment is not yet verified.
        // us-east-3 is waiting for the failed staging test and us-central-1, while us-west-1 is waiting only for us-central-1.
        // Only us-east-3 is verified, on revision1.
        // staging-test has 5 runs: one success without sources on revision1, one success from revision1 to revision2,
        // one success from revision2 to revision3 and two failures from revision1 to revision3.
        assertResponse(JobControllerApiHandlerHelper.runResponse(tester.controller(), new JobId(app.instanceId(), stagingTest), Optional.empty(), URI.create("https://some.url:43/root")), "staging-runs.json");
        assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), stagingTest).get().id(), "0"), "staging-test-log.json");
        assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json");
        assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")), "overview.json");

        var userApp = tester.newDeploymentContext(app.instanceId().tenant().value(), app.instanceId().application().value(), "user");
        userApp.runJob(devAwsUsEast2a, applicationPackage);
        assertResponse(JobControllerApiHandlerHelper.runResponse(tester.controller(), new JobId(userApp.instanceId(), devAwsUsEast2a), Optional.empty(), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json");
        assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), userApp.instanceId(), URI.create("https://some.url:43/root/")), "overview-user-instance.json");
        assertResponse(JobControllerApiHandlerHelper.overviewResponse(tester.controller(), app.application().id(), URI.create("https://some.url:43/root/")), "deployment-overview-2.json");

        tester.configServer().setLogStream(() -> "no more logs");
        assertResponse(JobControllerApiHandlerHelper.vespaLogsResponse(tester.jobs(), new RunId(app.instanceId(), stagingTest, 1), 0, false), "vespa.log");
        assertResponse(JobControllerApiHandlerHelper.vespaLogsResponse(tester.jobs(), new RunId(app.instanceId(), stagingTest, 1), 0, true), "vespa.log");
    }

    @Test
    void testDevResponses() {
        DeploymentTester tester = new DeploymentTester();
        var app = tester.newDeploymentContext();
        tester.clock().setInstant(Instant.EPOCH);

        ZoneId zone = DeploymentContext.devUsEast1.zone();
        tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
        tester.configServer().setLogStream(() -> "1554970337.935104\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n");
        assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json");

        tester.configServer().setLogStream(() -> "Nope, this won't be logged");
        tester.configServer().convergeServices(app.instanceId(), zone);
        tester.runner().run();
        assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), "8"), "dev-us-east-1-log-second-part.json");

        tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
        assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root")), "dev-overview.json");
    }

    @Test
    void testResponsesWithDirectDeployment() {
        var tester = new DeploymentTester();
        var app = tester.newDeploymentContext();
        tester.clock().setInstant(Instant.EPOCH);
        var region = "us-west-1";
        var applicationPackage = new ApplicationPackageBuilder().region(region).build();
        // Deploy directly to production zone, like integration tests.
        tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage);
        assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
                "jobs-direct-deployment.json");
    }

    @Test
    void testResponsesWithDryRunDeployment() {
        var tester = new DeploymentTester();
        var app = tester.newDeploymentContext();
        tester.clock().setInstant(Instant.EPOCH);
        var region = "us-west-1";
        var applicationPackage = new ApplicationPackageBuilder().region(region).build();
        // Deploy directly to production zone, like integration tests, with dryRun.
        tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage, true, true);
        assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
                "jobs-direct-deployment.json");
    }

    @Test
    void testEnclave() {
        var cloudAccount = CloudAccount.from("aws:123456789012");
        var applicationPackage = new ApplicationPackageBuilder()
                .cloudAccount(cloudAccount.value())
                .stagingTest()
                .systemTest()
                .region("aws-us-east-1c")
                .build();
        var tester = new DeploymentTester(new ControllerTester(SystemName.Public));
        tester.controllerTester().flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(cloudAccount.value()), String.class);
        tester.controllerTester().zoneRegistry().configureCloudAccount(cloudAccount, systemTest.zone(), stagingTest.zone(), ZoneId.from("prod.aws-us-east-1c"));

        var app = tester.newDeploymentContext();
        app.submit(applicationPackage).deploy();
        assertEquals(Optional.of(cloudAccount), tester.controllerTester().configServer().cloudAccount(app.deploymentIdIn(systemTest.zone())));

        assertResponse(JobControllerApiHandlerHelper.overviewResponse(tester.controller(), app.application().id(), URI.create("https://some.url:43/root/")), "overview-enclave.json");
    }

    private void assertResponse(HttpResponse response, String fileName) {
        try {
            Path path = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/").resolve(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            response.render(baos);
            if (fileName.endsWith(".json")) {
                byte[] actualJson = SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(baos.toByteArray()).get(), false);
                // Files.write(path, actualJson);
                byte[] expected = Files.readAllBytes(path);
                assertEquals(new String(SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(expected).get(), false), UTF_8),
                             new String(actualJson, UTF_8));
            }
            else {
                assertEquals(Files.readString(path),
                             baos.toString(UTF_8));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
