package merger

import (
	"context"
	"encoding/json"
	"strings"
	"testing"

	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

var testGVK = schema.GroupVersionKind{Group: "core.pingcap.com", Version: "v1alpha1", Kind: "TiDBGroup"}

type staticSchemaResolver struct {
	schema *apiextensionsv1.JSONSchemaProps
}

func (r staticSchemaResolver) Resolve(context.Context, schema.GroupVersionKind) (*apiextensionsv1.JSONSchemaProps, error) {
	return r.schema.DeepCopy(), nil
}

func TestMergerUsesExplicitRuleBeforeCRDAtomicMap(t *testing.T) {
	rootSchema := tiDBGroupMergeSchema()
	m, err := New(staticSchemaResolver{schema: rootSchema}, Rule{
		Group:    "core.pingcap.com",
		Path:     "/spec/template/spec/overlay/pod/spec/nodeSelector",
		Strategy: StrategyMergeMap,
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}

	live := mustObject(t, `{
  "spec":{"template":{"spec":{"overlay":{"pod":{"spec":{"nodeSelector":{
    "node-spec":"4C16G",
    "tidbcloud.com/node-pool-k8s-version":"1.35"
	  }}}}}}}}
	`)
	desired := mustObject(t, `{
  "spec":{"template":{"spec":{"overlay":{"pod":{"spec":{"nodeSelector":{
    "node-spec":"8C16G"
	  }}}}}}}}
	`)

	if err := m.Merge(context.Background(), testGVK, live, desired); err != nil {
		t.Fatalf("Merge: %v", err)
	}
	selector := nestedMap(t, live, "spec", "template", "spec", "overlay", "pod", "spec", "nodeSelector")
	if selector["node-spec"] != "8C16G" {
		t.Fatalf("node-spec = %v, want 8C16G", selector["node-spec"])
	}
	if selector["tidbcloud.com/node-pool-k8s-version"] != "1.35" {
		t.Fatalf("node-pool version = %v, want 1.35", selector["tidbcloud.com/node-pool-k8s-version"])
	}
}

func TestMergerUsesCRDListMapAndNestedListMapDefinitions(t *testing.T) {
	m, err := New(staticSchemaResolver{schema: tiDBGroupMergeSchema()})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	live := mustObject(t, `{
  "spec":{"template":{"spec":{"overlay":{"pod":{"spec":{"containers":[
    {"name":"tidb","image":"pingcap/tidb:existing","resources":{"requests":{"cpu":"3257m","memory":"13115Mi"}},"env":[
      {"name":"TZ","value":"UTC"},
      {"name":"GOMEMLIMIT","value":"11263MiB"}
    ]},
    {"name":"auditlog","image":"audit:existing"}
	  ]}}}}}}}
	`)
	desired := mustObject(t, `{
  "spec":{"template":{"spec":{"overlay":{"pod":{"spec":{"containers":[
    {"name":"tidb","resources":{"requests":{"cpu":"7220m"}},"env":[
      {"name":"GOMEMLIMIT","value":"11262MiB"}
    ]}
	  ]}}}}}}}
	`)

	if err := m.Merge(context.Background(), testGVK, live, desired); err != nil {
		t.Fatalf("Merge: %v", err)
	}
	containers := nestedSlice(t, live, "spec", "template", "spec", "overlay", "pod", "spec", "containers")
	if len(containers) != 2 {
		t.Fatalf("containers = %v, want tidb and auditlog", containers)
	}
	tidb := containers[0].(map[string]interface{})
	if tidb["image"] != "pingcap/tidb:existing" {
		t.Fatalf("tidb image = %v, want existing image", tidb["image"])
	}
	requests := nestedMap(t, tidb, "resources", "requests")
	if requests["cpu"] != "7220m" || requests["memory"] != "13115Mi" {
		t.Fatalf("requests = %v, want merged cpu and existing memory", requests)
	}
	env := nestedSlice(t, tidb, "env")
	if len(env) != 2 || env[0].(map[string]interface{})["name"] != "TZ" {
		t.Fatalf("env = %v, want TZ preserved", env)
	}
	if env[1].(map[string]interface{})["value"] != "11262MiB" {
		t.Fatalf("GOMEMLIMIT = %v, want updated value", env[1])
	}
}

func TestMergerUsesCRDAtomicAndSetDefinitions(t *testing.T) {
	atomic := "atomic"
	set := "set"
	root := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"atomicMap": {Type: "object", XMapType: &atomic},
		"features":  {Type: "array", XListType: &set},
	})
	m, err := New(staticSchemaResolver{schema: &root})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	live := map[string]interface{}{
		"atomicMap": map[string]interface{}{"keep": "no", "replace": "old"},
		"features":  []interface{}{"one", "two"},
	}
	desired := map[string]interface{}{
		"atomicMap": map[string]interface{}{"replace": "new"},
		"features":  []interface{}{"two", "three"},
	}

	if err := m.Merge(context.Background(), testGVK, live, desired); err != nil {
		t.Fatalf("Merge: %v", err)
	}
	atomicMap := live["atomicMap"].(map[string]interface{})
	if len(atomicMap) != 1 || atomicMap["replace"] != "new" {
		t.Fatalf("atomicMap = %v, want CRD-defined replacement", atomicMap)
	}
	features := live["features"].([]interface{})
	if strings.Join([]string{features[0].(string), features[1].(string), features[2].(string)}, ",") != "one,two,three" {
		t.Fatalf("features = %v, want CRD-defined set union", features)
	}
}

func TestNewRejectsInvalidRules(t *testing.T) {
	_, err := New(staticSchemaResolver{schema: &apiextensionsv1.JSONSchemaProps{}}, Rule{
		Path:     "/items",
		Strategy: StrategyMergeListMap,
	})
	if err == nil || !strings.Contains(err.Error(), "listMapKeys are required") {
		t.Fatalf("New error = %v, want list-map key validation", err)
	}
}

func tiDBGroupMergeSchema() *apiextensionsv1.JSONSchemaProps {
	atomic := "atomic"
	listMap := "map"
	envSchema := apiextensionsv1.JSONSchemaProps{
		Type:         "array",
		XListType:    &listMap,
		XListMapKeys: []string{"name"},
		Items: &apiextensionsv1.JSONSchemaPropsOrArray{Schema: &apiextensionsv1.JSONSchemaProps{
			Type: "object",
		}},
	}
	containerSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"env": envSchema,
	})
	containersSchema := apiextensionsv1.JSONSchemaProps{
		Type:         "array",
		XListType:    &listMap,
		XListMapKeys: []string{"name"},
		Items:        &apiextensionsv1.JSONSchemaPropsOrArray{Schema: &containerSchema},
	}
	podSpecSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"nodeSelector": {Type: "object", XMapType: &atomic},
		"containers":   containersSchema,
	})
	podSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"spec": podSpecSchema,
	})
	overlaySchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"pod": podSchema,
	})
	templateSpecSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"overlay": overlaySchema,
	})
	templateSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"spec": templateSpecSchema,
	})
	specSchema := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"template": templateSchema,
	})
	root := objectSchema(map[string]apiextensionsv1.JSONSchemaProps{
		"spec": specSchema,
	})
	return &root
}

func objectSchema(properties map[string]apiextensionsv1.JSONSchemaProps) apiextensionsv1.JSONSchemaProps {
	return apiextensionsv1.JSONSchemaProps{Type: "object", Properties: properties}
}

func mustObject(t *testing.T, value string) map[string]interface{} {
	t.Helper()
	var object map[string]interface{}
	if err := json.Unmarshal([]byte(value), &object); err != nil {
		t.Fatalf("decode object: %v", err)
	}
	return object
}

func nestedMap(t *testing.T, object map[string]interface{}, fields ...string) map[string]interface{} {
	t.Helper()
	current := object
	for _, field := range fields {
		next, ok := current[field].(map[string]interface{})
		if !ok {
			t.Fatalf("%s is %T, want object", strings.Join(fields, "."), current[field])
		}
		current = next
	}
	return current
}

func nestedSlice(t *testing.T, object map[string]interface{}, fields ...string) []interface{} {
	t.Helper()
	parent := nestedMap(t, object, fields[:len(fields)-1]...)
	value, ok := parent[fields[len(fields)-1]].([]interface{})
	if !ok {
		t.Fatalf("%s is %T, want array", strings.Join(fields, "."), parent[fields[len(fields)-1]])
	}
	return value
}
