package tidbgateway

import (
	"io"
	"os"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func TestGatewayServiceMonitorManifests(t *testing.T) {
	t.Parallel()

	monitor := singleServiceMonitor(t, "service-monitor-inner.yaml")

	name, found, err := unstructured.NestedString(monitor, "metadata", "name")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "tidb-gateway-inner", name)
	require.Equal(t, []string{"tidb-gateway-metrics-inner"}, serviceMonitorControlPlaneValues(t, monitor))
	assertGatewayMetricsEndpoint(t, monitor)
}

func TestInnerMetricsServiceManifest(t *testing.T) {
	t.Parallel()

	svc := singleService(t, "service-metrics-inner.yaml")

	name, found, err := unstructured.NestedString(svc, "metadata", "name")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "gateway-metrics-inner", name)

	labels, found, err := unstructured.NestedStringMap(svc, "metadata", "labels")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "tidb-gateway-metrics-inner", labels["control-plane"])

	selector, found, err := unstructured.NestedStringMap(svc, "spec", "selector")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "tidb-gateway-inner", selector["control-plane"])
}

func TestTidbGatewayPulumiLoadsInnerMetricsServiceWithInnerGateway(t *testing.T) {
	t.Parallel()

	sourceBytes, err := os.ReadFile("../../src/tidb-gateway.ts")
	require.NoError(t, err)
	source := string(sourceBytes)

	innerMetricsIndex := strings.Index(source, `file: "../manifests/tidb-gateway/service-metrics-inner.yaml"`)
	require.NotEqual(t, -1, innerMetricsIndex, "tidb-gateway.ts must load the inner metrics Service manifest")

	blockStart := strings.LastIndex(source[:innerMetricsIndex], "new k8s.yaml.ConfigFile(")
	require.NotEqual(t, -1, blockStart)
	blockEndOffset := strings.Index(source[innerMetricsIndex:], "\n  );")
	require.NotEqual(t, -1, blockEndOffset)
	block := source[blockStart : innerMetricsIndex+blockEndOffset]

	require.Contains(t, block, "`${clusterName}-tidb-gateway-metrics-inner`")
	require.Contains(t, block, "dependsOn: [gatewayInnerDeployment]")
}

func TestTidbGatewayPulumiLoadsInnerServiceMonitorWithInnerGateway(t *testing.T) {
	t.Parallel()

	sourceBytes, err := os.ReadFile("../../src/tidb-gateway.ts")
	require.NoError(t, err)
	source := string(sourceBytes)

	innerMonitorIndex := strings.Index(source, `file: "../manifests/tidb-gateway/service-monitor-inner.yaml"`)
	require.NotEqual(t, -1, innerMonitorIndex, "tidb-gateway.ts must load the inner gateway ServiceMonitor manifest")

	blockStart := strings.LastIndex(source[:innerMonitorIndex], "new k8s.yaml.ConfigFile(")
	require.NotEqual(t, -1, blockStart)
	blockEndOffset := strings.Index(source[innerMonitorIndex:], "\n  );")
	require.NotEqual(t, -1, blockEndOffset)
	block := source[blockStart : innerMonitorIndex+blockEndOffset]

	require.Contains(t, block, "`${clusterName}-tidb-gateway-monitor-inner`")
	require.Contains(t, block, "dependsOn: [gatewayInnerDeployment]")
}

func singleServiceMonitor(t *testing.T, path string) map[string]interface{} {
	t.Helper()

	docs := readYAMLDocuments(t, path)
	var serviceMonitors []map[string]interface{}
	for _, doc := range docs {
		kind, found, err := unstructured.NestedString(doc, "kind")
		require.NoError(t, err)
		if found && kind == "ServiceMonitor" {
			serviceMonitors = append(serviceMonitors, doc)
		}
	}

	require.Len(t, serviceMonitors, 1)
	return serviceMonitors[0]
}

func singleService(t *testing.T, path string) map[string]interface{} {
	t.Helper()

	docs := readYAMLDocuments(t, path)
	var services []map[string]interface{}
	for _, doc := range docs {
		kind, found, err := unstructured.NestedString(doc, "kind")
		require.NoError(t, err)
		if found && kind == "Service" {
			services = append(services, doc)
		}
	}

	require.Len(t, services, 1)
	return services[0]
}

func readYAMLDocuments(t *testing.T, path string) []map[string]interface{} {
	t.Helper()

	file, err := os.Open(path)
	require.NoError(t, err)
	defer func() {
		require.NoError(t, file.Close())
	}()

	decoder := yaml.NewDecoder(file)
	var docs []map[string]interface{}
	for {
		doc := map[string]interface{}{}
		err := decoder.Decode(&doc)
		if err == io.EOF {
			break
		}
		require.NoError(t, err)
		if len(doc) == 0 {
			continue
		}
		docs = append(docs, doc)
	}

	return docs
}

func serviceMonitorControlPlaneValues(t *testing.T, monitor map[string]interface{}) []string {
	t.Helper()

	expressions, found, err := unstructured.NestedSlice(monitor, "spec", "selector", "matchExpressions")
	require.NoError(t, err)
	require.True(t, found)

	for _, expression := range expressions {
		expressionMap, ok := expression.(map[string]interface{})
		require.True(t, ok)
		key, found, err := unstructured.NestedString(expressionMap, "key")
		require.NoError(t, err)
		if found && key == "control-plane" {
			values, found, err := unstructured.NestedStringSlice(expressionMap, "values")
			require.NoError(t, err)
			require.True(t, found)
			return values
		}
	}

	require.FailNow(t, "ServiceMonitor selector must match the control-plane label")
	return nil
}

func assertGatewayMetricsEndpoint(t *testing.T, monitor map[string]interface{}) {
	t.Helper()

	endpoints, found, err := unstructured.NestedSlice(monitor, "spec", "endpoints")
	require.NoError(t, err)
	require.True(t, found)
	require.Len(t, endpoints, 1)

	endpoint, ok := endpoints[0].(map[string]interface{})
	require.True(t, ok)

	path, found, err := unstructured.NestedString(endpoint, "path")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "/metrics", path)

	port, found, err := unstructured.NestedString(endpoint, "port")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "metric", port)

	interval, found, err := unstructured.NestedString(endpoint, "interval")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "30s", interval)

	honorLabels, found, err := unstructured.NestedBool(endpoint, "honorLabels")
	require.NoError(t, err)
	require.True(t, found)
	require.True(t, honorLabels)
}
