package externalissuer

import (
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/linkerd/linkerd2/pkg/k8s"
	"github.com/linkerd/linkerd2/testutil"
	corev1 "k8s.io/api/core/v1"
)

var TestHelper *testutil.TestHelper

const (
	TestAppBackendDeploymentName = "backend"
	TestAppNamespaceSuffix       = "external-issuer-app-test"
)

func TestMain(m *testing.M) {
	TestHelper = testutil.NewTestHelper()
	if !TestHelper.ExternalIssuer() {
		fmt.Fprintln(os.Stdout, "Skiping as --external-issuer=false")
		os.Exit(0)
	}
	os.Exit(m.Run())
}

func TestExternalIssuer(t *testing.T) {
	verifyInstallApp(t)
	verifyAppWorksBeforeCertRotation(t)
	verifyRotateExternalCerts(t)
	verifyIdentityServiceReloadsIssuerCert(t)
	ensureNewCSRSAreServed()
	verifyAppWorksAfterCertRotation(t)
}

func verifyInstallApp(t *testing.T) {
	out, stderr, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/external_issuer_application.yaml")
	if err != nil {
		t.Fatalf("linkerd inject command failed\n%s\n%s", out, stderr)
	}

	prefixedNs := TestHelper.GetTestNamespace(TestAppNamespaceSuffix)
	err = TestHelper.CreateDataPlaneNamespaceIfNotExists(prefixedNs, nil)
	if err != nil {
		t.Fatalf("failed to create %s namespace: %s", prefixedNs, err)
	}
	out, err = TestHelper.KubectlApply(out, prefixedNs)
	if err != nil {
		t.Fatalf("kubectl apply command failed\n%s", out)
	}

	if err := TestHelper.CheckPods(prefixedNs, TestAppBackendDeploymentName, 1); err != nil {
		t.Error(err)
	}

	if err := TestHelper.CheckPods(prefixedNs, "slow-cooker", 1); err != nil {
		t.Error(err)
	}
}

func checkAppWoks(t *testing.T) error {
	return TestHelper.RetryFor(40*time.Second, func() error {
		args := []string{"stat", "deploy", "-n", TestHelper.GetTestNamespace(TestAppNamespaceSuffix), "--from", "deploy/slow-cooker", "-t", "1m"}
		out, stderr, err := TestHelper.LinkerdRun(args...)
		if err != nil {
			return fmt.Errorf("Unexpected stat error: %s\n%s\n%s", err, out, stderr)
		}
		rowStats, err := testutil.ParseRows(out, 1, 8)
		if err != nil {
			return fmt.Errorf("%s\n%s", err, stderr)
		}

		stat := rowStats[TestAppBackendDeploymentName]
		if stat.Success != "100.00%" {
			t.Fatalf("Expected no errors in test app but got [%s] succes rate", stat.Success)
		}
		return nil
	})

}

func verifyAppWorksBeforeCertRotation(t *testing.T) {
	err := checkAppWoks(t)
	if err != nil {
		t.Fatalf("Received error while ensuring test app works (before cert rotation): %s", err)
	}
}

func verifyRotateExternalCerts(t *testing.T) {
	// We rotate the certificates here by simply grabbing
	// the key and cert values from the temporary secret we have
	// created
	secretWithUpdatedData, err := TestHelper.KubernetesHelper.GetSecret(TestHelper.GetLinkerdNamespace(), k8s.IdentityIssuerSecretName+"-new")
	if err != nil {
		t.Fatalf("failed to fetch new secret data resource: %s", err)
	}

	roots := secretWithUpdatedData.Data[k8s.IdentityIssuerTrustAnchorsNameExternal]
	crt := secretWithUpdatedData.Data[corev1.TLSCertKey]
	key := secretWithUpdatedData.Data[corev1.TLSPrivateKeyKey]

	if err = TestHelper.CreateTLSSecret(k8s.IdentityIssuerSecretName, string(roots), string(crt), string(key)); err != nil {
		t.Fatalf("failed to update linkerd-identity-issuer resource: %s", err)
	}
}

func verifyIdentityServiceReloadsIssuerCert(t *testing.T) {
	// check that the identity service has received an IssuerUpdated event
	err := TestHelper.RetryFor(90*time.Second, func() error {
		out, err := TestHelper.Kubectl("",
			"--namespace", TestHelper.GetLinkerdNamespace(),
			"get", "events", "--field-selector", "reason=IssuerUpdated", "-ojson",
		)
		if err != nil {
			t.Errorf("kubectl get events command failed with %s\n%s", err, out)
		}

		events, err := testutil.ParseEvents(out)
		if err != nil {
			return err
		}

		if len(events) != 1 {
			return fmt.Errorf("expected just one event but got %d", len(events))
		}

		expectedEventMessage := "Updated identity issuer"
		issuerUpdatedEvent := events[0]

		if issuerUpdatedEvent.Message != expectedEventMessage {
			return fmt.Errorf("expected event message [%s] but got [%s]", expectedEventMessage, issuerUpdatedEvent.Message)

		}
		return nil
	})
	if err != nil {
		t.Fatal(err.Error())
	}

}

func ensureNewCSRSAreServed() {
	// this is to ensure new certs have been issued by the identity service.
	// we know that this will happen because the issuance lifetime is set to 15s.
	// Possible improvement is to provide a more deterministic way of checking that.
	// Perhaps we can emit k8s events when the identity service processed a CSR.
	time.Sleep(20 * time.Second)
}

func verifyAppWorksAfterCertRotation(t *testing.T) {
	err := checkAppWoks(t)
	if err != nil {
		t.Fatalf("Received error while ensuring test app works (after cert rotation): %s", err)
	}
}
