/*
 * Copyright (c) 2016 The original author or authors
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *      The Eclipse Public License is available at
 *      http://www.eclipse.org/legal/epl-v10.html
 *
 *      The Apache License v2.0 is available at
 *      http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */
package io.vertx.ext.consul.suite;

import grpc.health.v1.HealthCheck;
import grpc.health.v1.HealthGrpc;
import io.grpc.stub.StreamObserver;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.ext.consul.*;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import io.vertx.grpc.VertxServer;
import io.vertx.grpc.VertxServerBuilder;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static io.vertx.ext.consul.Utils.*;
import static io.vertx.test.core.TestUtils.randomAlphaString;

/**
 * @author <a href="mailto:[email protected]">Ruslan Sennov</a>
 */
@RunWith(VertxUnitRunner.class)
public abstract class ChecksBase extends ConsulTestBase {

  abstract String createCheck(CheckOptions opts);

  abstract void createCheck(TestContext tc, CheckOptions opts, Handler<String> idHandler);

  @Test
  public void ttlCheckLifecycle(TestContext tc) {
    CheckOptions opts = new CheckOptions()
      .setTtl("2s")
      .setName(randomAlphaString(10));
    String checkId = createCheck(opts);

    Check check;

    runAsync(h -> ctx.writeClient().warnCheckWithNote(checkId, "warn", h));
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.WARNING, check.getStatus());
    assertEquals("warn", check.getOutput());

    runAsync(h -> ctx.writeClient().failCheckWithNote(checkId, "fail", h));
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.CRITICAL, check.getStatus());
    assertEquals("fail", check.getOutput());

    runAsync(h -> ctx.writeClient().passCheckWithNote(checkId, "pass", h));
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.PASSING, check.getStatus());
    assertEquals("pass", check.getOutput());

    sleep(vertx, 3000);

    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.CRITICAL, check.getStatus());

    runAsync(h -> ctx.writeClient().deregisterCheck(checkId, h));
  }

  @Test
  public void httpCheckLifecycle() {
    HttpHealthReporter reporter = new HttpHealthReporterWithHeaderCheck(vertx);

    HashMap<String, List<String>> headers = new HashMap<>();
    headers.put(
      HttpHealthReporterWithHeaderCheck.TEST_HEADER_NAME,
      Collections.singletonList(HttpHealthReporterWithHeaderCheck.TEST_HEADER_VALUE)
    );

    CheckOptions opts = new CheckOptions()
      .setHttp("http://localhost:" + reporter.port())
      .setInterval("2s")
      .setHeaders(headers)
      .setName("checkName");
    String checkId = createCheck(opts);

    sleep(vertx, 3000);
    Check check = getCheckInfo(checkId);
    assertEquals(CheckStatus.PASSING, check.getStatus());

    reporter.setStatus(CheckStatus.WARNING);
    sleep(vertx, 3000);
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.WARNING, check.getStatus());

    reporter.setStatus(CheckStatus.CRITICAL);
    sleep(vertx, 3000);
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.CRITICAL, check.getStatus());

    reporter.close();

    runAsync(h -> ctx.writeClient().deregisterCheck(checkId, h));
  }

  @Test
  public void grpcCheckLifecycle(TestContext tc) {
    if (ctx.consulVersion().compareTo("1.0.3") < 0) {
      System.out.println("skip " + ctx.consulVersion() + " version");
      return;
    }
    GrpcHealthReporter reporter = new GrpcHealthReporter(vertx);
    Async async = tc.async();

    CheckOptions opts = new CheckOptions()
      .setGrpc("localhost:" + reporter.port() + "/testee")
      .setInterval("2s")
      .setName("checkName");
    String checkId = createCheck(opts);

    reporter.start(tc.asyncAssertSuccess(v1 -> {
      vertx.setTimer(3000, t1 -> {
        getCheckInfo(tc, checkId, c1 -> {
          tc.assertEquals(CheckStatus.PASSING, c1.getStatus());
          reporter.setStatus(HealthCheck.HealthCheckResponse.ServingStatus.NOT_SERVING);

          vertx.setTimer(3000, t2 -> {
            getCheckInfo(tc, checkId, c2 -> {
              tc.assertEquals(CheckStatus.CRITICAL, c2.getStatus());

              reporter.close(tc.asyncAssertSuccess(v2 -> {
                ctx.writeClient().deregisterCheck(checkId, tc.asyncAssertSuccess(v -> async.complete()));
              }));
            });
          });

        });
      });
    }));
  }

  @Test
  public void tcpCheckLifecycle() {
    HttpHealthReporter reporter = new HttpHealthReporterImpl(vertx);

    CheckOptions opts = new CheckOptions()
      .setTcp("localhost:" + reporter.port())
      .setInterval("2s")
      .setName("checkName");
    String checkId = createCheck(opts);

    sleep(vertx, 3000);
    Check check = getCheckInfo(checkId);
    assertEquals(CheckStatus.PASSING, check.getStatus());

    reporter.close();
    sleep(vertx, 3000);
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.CRITICAL, check.getStatus());

    runAsync(h -> ctx.writeClient().deregisterCheck(checkId, h));
  }

  @Test
  public void scriptCheckLifecycle() {
    ScriptHealthReporter reporter = new ScriptHealthReporter();

    CheckOptions opts = new CheckOptions()
      .setScriptArgs(reporter.scriptArgs())
      .setInterval("2s")
      .setName("checkName");
    String checkId = createCheck(opts);

    sleep(vertx, 3000);
    Check check = getCheckInfo(checkId);
    assertEquals(CheckStatus.PASSING, check.getStatus());

    reporter.setStatus(CheckStatus.WARNING);
    sleep(vertx, 3000);
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.WARNING, check.getStatus());

    reporter.setStatus(CheckStatus.CRITICAL);
    sleep(vertx, 3000);
    check = getCheckInfo(checkId);
    assertEquals(CheckStatus.CRITICAL, check.getStatus());

    runAsync(h -> ctx.writeClient().deregisterCheck(checkId, h));
  }

  Check getCheckInfo(String id) {
    List<Check> checks = getAsync(h -> ctx.writeClient().localChecks(h));
    return checks.stream()
      .filter(check -> check.getId().equals(id))
      .findFirst()
      .get();
  }

  void getCheckInfo(TestContext tc, String id, Handler<Check> resultHandler) {
    ctx.writeClient().localChecks(tc.asyncAssertSuccess(list -> {
      resultHandler.handle(list.stream()
        .filter(check -> check.getId().equals(id))
        .findFirst()
        .orElseThrow(NoSuchElementException::new));
    }));
  }

  private static class ScriptHealthReporter {

    private File healthStatusFile;
    private File scriptFile;

    ScriptHealthReporter() {
      try {
        Path scriptDir = Files.createTempDirectory("vertx-consul-script-dir-");
        healthStatusFile = new File(scriptDir.toFile(), "status");
        String scriptName = "health_script." + (Utils.isWindows() ? "bat" : "sh");
        String scriptContent = Utils.readResource(scriptName)
          .replace("%STATUS_FILE%", healthStatusFile.getAbsolutePath());
        scriptFile = new File(scriptDir.toFile(), scriptName);
        PrintStream out = new PrintStream(scriptFile);
        out.print(scriptContent);
        out.close();
        scriptFile.setExecutable(true);
      } catch (Exception e) {
        e.printStackTrace();
      }
      setStatus(CheckStatus.PASSING);
    }

    String scriptPath() {
      return scriptFile.getAbsolutePath();
    }

    List<String> scriptArgs() {
      List<String> scriptArgs = new ArrayList<>();
      scriptArgs.add(scriptPath());
      return scriptArgs;
    }

    void setStatus(CheckStatus status) {
      int statusCode;
      switch (status) {
        case PASSING:
          statusCode = 0;
          break;
        case WARNING:
          statusCode = 1;
          break;
        default:
          statusCode = 42;
          break;
      }
      try {
        PrintStream out = new PrintStream(healthStatusFile);
        out.print(statusCode);
        out.close();
      } catch (FileNotFoundException e) {
        e.printStackTrace();
      }
    }
  }

  private static abstract class HttpHealthReporter {

    private final HttpServer server;
    private final int port;
    protected CheckStatus status = CheckStatus.PASSING;

    HttpHealthReporter(Vertx vertx) {
      this.port = Utils.getFreePort();
      CountDownLatch latch = new CountDownLatch(1);
      this.server = vertx.createHttpServer().requestHandler(this::handle).listen(port, h -> latch.countDown());
      try {
        latch.await(10, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    abstract void handle(HttpServerRequest request);

    int port() {
      return port;
    }

    void close() {
      server.close();
    }

    protected int statusCode(CheckStatus status) {
      switch (status) {
        case PASSING:
          return 200;
        case WARNING:
          return 429;
        default:
          return 500;
      }
    }

    void setStatus(CheckStatus status) {
      this.status = status;
    }

  }

  private static class HttpHealthReporterImpl extends HttpHealthReporter {

    HttpHealthReporterImpl(Vertx vertx) {
      super(vertx);
    }

    @Override
    void handle(HttpServerRequest request) {
      request.response()
        .setStatusCode(statusCode(status))
        .end(status.name());
    }

  }

  private static class HttpHealthReporterWithHeaderCheck extends HttpHealthReporter {

    final static String TEST_HEADER_NAME = "test";
    final static String TEST_HEADER_VALUE = "foo";

    HttpHealthReporterWithHeaderCheck(Vertx vertx) {
      super(vertx);
    }

    @Override
    void handle(HttpServerRequest request) {
      String headerValue = request.getHeader(TEST_HEADER_NAME);
      Assert.assertEquals(TEST_HEADER_VALUE, headerValue);
      request.response()
        .setStatusCode(statusCode(status))
        .end(status.name());
    }

  }

  private static class GrpcHealthReporter {

    private final VertxServer server;
    private final int port;

    private HealthCheck.HealthCheckResponse.ServingStatus status = HealthCheck.HealthCheckResponse.ServingStatus.SERVING;

    GrpcHealthReporter(Vertx vertx) {
      this.port = Utils.getFreePort();
      HealthGrpc.HealthImplBase service = new HealthGrpc.HealthImplBase() {
        @Override
        public void check(HealthCheck.HealthCheckRequest request, StreamObserver<HealthCheck.HealthCheckResponse> response) {
          response.onNext(HealthCheck.HealthCheckResponse.newBuilder()
            .setStatus(status)
            .build());
          response.onCompleted();
        }
      };
      server = VertxServerBuilder
        .forPort(vertx, port)
        .addService(service)
        .build();
    }

    int port() {
      return port;
    }

    void setStatus(HealthCheck.HealthCheckResponse.ServingStatus status) {
      this.status = status;
    }

    void start(Handler<AsyncResult<Void>> h) {
      server.start(h);
    }

    void close(Handler<AsyncResult<Void>> h) {
      server.shutdown(h);
    }

  }
}