package norelay

import (
	"context"
	"fmt"
	"net"
	"os"
	"strings"
	"testing"

	"github.com/linkerd/linkerd2/testutil"
)

var TestHelper *testutil.TestHelper

func TestMain(m *testing.M) {
	TestHelper = testutil.NewTestHelper()
	// Block test execution until control plane is running
	TestHelper.WaitUntilDeployReady(testutil.LinkerdDeployReplicasEdge)
	os.Exit(m.Run())
}

// TestNoRelay verifies that hitting the proxy's outbound port doesn't result
// in an open relay, by trying to leverage the l5d-dst-override header in an
// ingress proxy.
func TestNoRelay(t *testing.T) {
	ctx := context.Background()
	deployments := getDeployments(t)
	TestHelper.WithDataPlaneNamespace(ctx, "norelay-test", map[string]string{}, t, func(t *testing.T, ns string) {
		for name, res := range deployments {
			out, err := TestHelper.KubectlApply(res, ns)
			if err != nil {
				testutil.AnnotatedFatalf(t, "unexpected error",
					"unexpected error with deployment %s: %v output:\n%s",
					name, err, out,
				)
			}
		}

		for name := range deployments {
			err := TestHelper.CheckPods(ctx, ns, name, 1)
			if err != nil {
				//nolint:errorlint
				if rce, ok := err.(*testutil.RestartCountError); ok {
					testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
				} else {
					testutil.AnnotatedError(t, "CheckPods timed-out", err)
				}
			}
		}

		relayIPPort := getPodIPPort(t, ns, "app=server-relay", 4140)
		o, err := TestHelper.Kubectl(
			"", "-n", ns, "exec", "deploy/client",
			"--", "curl", "-f", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIPPort,
		)
		if err == nil || err.Error() != "exit status 22" {
			testutil.AnnotatedFatalf(t, "no error or unexpected error returned",
				"no error or unexpected error returned: %s\n%s", o, err)
		}
	})
}

// TestRelay validates the previous test by running the same scenario but
// forcing an open relay by changing the value of
// LINKERD2_PROXY_OUTBOUND_LISTEN_ADDRS from 127.0.0.1:4140,[::1]:4140 to
// 0.0.0.0:4140, which is not possible without manually changing the injected
// proxy yaml
//
// We don't care if this behavior breaks--it's not a supported configuration.
// However, this test is oddly useful in finding bugs in ingress-mode proxy
// configurations, so we keep it around. ¯\_(ツ)_/¯
func TestRelay(t *testing.T) {
	ctx := context.Background()
	deployments := getDeployments(t)

	// account for both when IPv6 is enabled or disabled
	deployments["server-relay"] = strings.ReplaceAll(
		deployments["server-relay"],
		"127.0.0.1:4140,[::1]:4140",
		"'[::]:4140'",
	)
	deployments["server-relay"] = strings.ReplaceAll(
		deployments["server-relay"],
		"127.0.0.1:4140",
		"0.0.0.0:4140",
	)

	TestHelper.WithDataPlaneNamespace(ctx, "relay-test", map[string]string{}, t, func(t *testing.T, ns string) {
		for name, res := range deployments {
			out, err := TestHelper.KubectlApply(res, ns)
			if err != nil {
				testutil.AnnotatedFatalf(t, "unexpected error",
					"unexpected error with deployment %s: %v output:\n%s",
					name, err, out,
				)
			}
		}
		waitForPods(t, ctx, ns, deployments)
		relayIPPort := getPodIPPort(t, ns, "app=server-relay", 4140)

		// Send a request to the outbound proxy port with a header that should route internally.
		o, err := TestHelper.Kubectl(
			"", "-n", ns, "exec", "deploy/client",
			"--", "curl", "-fsv", "-H", "l5d-dst-override: server-hello."+ns+".svc.cluster.local:8080", "http://"+relayIPPort,
		)
		if err != nil {
			log, err := TestHelper.Kubectl(
				"", "logs",
				"-n", ns,
				"-l", "app=server-relay",
				"-c", "linkerd-proxy",
				"--tail=1000",
			)
			if err != nil {
				log = fmt.Sprintf("failed to retrieve server-relay logs: %s", err)
			}
			testutil.AnnotatedFatalf(t, "unexpected error returned",
				"unexpected error returned: %s\n%s\n---\n%s", o, err, log)
		}
		if !strings.Contains(o, "HELLO-FROM-SERVER") {
			testutil.AnnotatedFatalf(t, "unexpected response returned",
				"unexpected response returned: %s", o)
		}
	})
}

func getDeployments(t *testing.T) map[string]string {
	deploys := make(map[string]string)
	var err error

	// server-hello is injected normally
	deploys["server-hello"], err = TestHelper.LinkerdRun("inject", "testdata/server-hello.yml")
	if err != nil {
		testutil.AnnotatedFatal(t, "unexpected error", err)
	}

	// server-relay is injected in ingress mode, manually
	deploys["server-relay"], err = TestHelper.LinkerdRun(
		"inject", "--manual", "--ingress",
		"--proxy-log-level=linkerd=debug,info",
		"testdata/server-relay.yml")
	if err != nil {
		testutil.AnnotatedFatal(t, "unexpected error", err)
	}

	// client is not injected
	deploys["client"], err = testutil.ReadFile("testdata/client.yml")
	if err != nil {
		testutil.AnnotatedFatalf(t, "failed to read 'client.yml'",
			"failed to read 'client.yml': %s", err)
	}

	return deploys
}

func getPodIPPort(t *testing.T, ns, selector string, port int) string {
	t.Helper()
	ip, err := TestHelper.Kubectl(
		"", "-n", ns, "get", "po", "-l", selector,
		"-o", "jsonpath='{range .items[*]}{@.status.podIP}{end}'",
	)
	if err != nil {
		testutil.AnnotatedFatalf(t, "failed to retrieve pod IP",
			"failed to retrieve pod IP: %s", err)
	}
	ip = strings.Trim(ip, "'")
	parsedIP := net.ParseIP(ip)
	if parsedIP == nil {
		testutil.AnnotatedFatalf(t, "invalid pod IP",
			"invalid pod IP: %s", err)
	}
	if parsedIP.To4() != nil {
		return fmt.Sprintf("%s:%d", ip, port)
	}
	return fmt.Sprintf("[%s]:%d", ip, port)
}

func waitForPods(t *testing.T, ctx context.Context, ns string, deployments map[string]string) {
	t.Helper()
	for name := range deployments {
		err := TestHelper.CheckPods(ctx, ns, name, 1)
		if err != nil {
			//nolint:errorlint
			if rce, ok := err.(*testutil.RestartCountError); ok {
				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
			} else {
				testutil.AnnotatedError(t, "CheckPods timed-out", err)
			}
		}
	}
}
