package gitops

import (
	"encoding/json"
	"errors"
	"strings"
	"testing"
)

func strp(s string) *string { return &s }
func intp(i int) *int       { return &i }

func TestParseSpec(t *testing.T) {
	s, err := ParseSpec([]byte("version: 1\ndeploy:\n  port: 8080\n  deploy_strategy: blue-green\n"))
	if err != nil {
		t.Fatalf("valid parse: %v", err)
	}
	if s.Version != 1 || s.Deploy.Port == nil || *s.Deploy.Port != 8080 {
		t.Fatalf("unexpected spec: %+v", s)
	}
	if s.Deploy.Healthcheck != nil {
		t.Fatalf("omitted healthcheck must stay nil")
	}

	// Unknown keys are rejected — incl. an attempt to declare env (out of v1).
	if _, err := ParseSpec([]byte("version: 1\ndeploy:\n  env:\n    FOO: bar\n")); err == nil {
		t.Fatalf("expected unknown-field error for deploy.env")
	}
	if _, err := ParseSpec([]byte("version: 1\nworkloads: []\n")); err == nil {
		t.Fatalf("expected unknown-field error for top-level workloads")
	}
	if _, err := ParseSpec([]byte("version: 2\n")); err == nil {
		t.Fatalf("expected unsupported-version error")
	}
	if _, err := ParseSpec(nil); err == nil {
		t.Fatalf("expected empty-file error")
	}
}

func TestBuildPlan_SourceAware(t *testing.T) {
	spec := Spec{Version: 1, Deploy: DeploySpec{
		Port: intp(8080), Healthcheck: strp("/h"), DeployStrategy: strp("blue-green"),
	}}

	df := BuildPlan(spec, SourceDockerfile).SourceConfigPatch
	if df[keyPort] != 8080 || df[keyHealthcheck] != "/h" || df[keyDeployStrategy] != "blue-green" {
		t.Fatalf("dockerfile patch wrong: %+v", df)
	}

	// static has no port/healthcheck — they must NOT leak into its patch.
	st := BuildPlan(spec, SourceStatic).SourceConfigPatch
	if _, ok := st[keyPort]; ok {
		t.Fatalf("static patch must not contain port")
	}
	if _, ok := st[keyHealthcheck]; ok {
		t.Fatalf("static patch must not contain healthcheck")
	}
	if st[keyDeployStrategy] != "blue-green" {
		t.Fatalf("static should keep deploy_strategy: %+v", st)
	}

	if IsEligibleSource("image") || IsEligibleSource("compose") {
		t.Fatalf("only dockerfile/static are GitOps-eligible in v1")
	}
	if !IsEligibleSource(SourceDockerfile) || !IsEligibleSource(SourceStatic) {
		t.Fatalf("dockerfile + static must be eligible")
	}
}

func TestMergeAndValidate_PreservesOmittedFields(t *testing.T) {
	live := json.RawMessage(`{"repo_owner":"o","repo_name":"r","port":3000,"healthcheck":"/old","deploy_strategy":""}`)
	spec := Spec{Version: 1, Deploy: DeploySpec{Port: intp(8080)}} // only port declared
	merged, err := MergeAndValidate(live, BuildPlan(spec, SourceDockerfile), func(json.RawMessage) error { return nil })
	if err != nil {
		t.Fatal(err)
	}
	var m map[string]any
	if err := json.Unmarshal(merged, &m); err != nil {
		t.Fatal(err)
	}
	if m["port"].(float64) != 8080 {
		t.Fatalf("declared port not applied: %v", m["port"])
	}
	if m["healthcheck"] != "/old" {
		t.Fatalf("undeclared healthcheck must be preserved, got %v", m["healthcheck"])
	}
	if m["repo_owner"] != "o" {
		t.Fatalf("untouched repo_owner lost")
	}
}

func TestMergeAndValidate_RejectsInvalidMergedConfig(t *testing.T) {
	live := json.RawMessage(`{"port":3000}`)
	spec := Spec{Version: 1, Deploy: DeploySpec{DeployStrategy: strp("rolling")}}
	_, err := MergeAndValidate(live, BuildPlan(spec, SourceDockerfile), func(c json.RawMessage) error {
		var x struct {
			DeployStrategy string `json:"deploy_strategy"`
		}
		_ = json.Unmarshal(c, &x)
		if x.DeployStrategy == "rolling" {
			return errors.New("invalid deploy_strategy")
		}
		return nil
	})
	if err == nil {
		t.Fatalf("expected the merged config to be rejected as a whole")
	}
}

func TestDrift_DeclaredOnly_WithNormalization(t *testing.T) {
	// live: port 3000, healthcheck "/h", strategy "" (== recreate effective).
	live := json.RawMessage(`{"port":3000,"healthcheck":"/h","deploy_strategy":"","registry_name":"x"}`)
	// declare: port (changed) + deploy_strategy "recreate" (equal to "" -> no drift).
	spec := Spec{Version: 1, Deploy: DeploySpec{Port: intp(8080), DeployStrategy: strp("recreate")}}
	d, err := Drift(spec, live, SourceDockerfile)
	if err != nil {
		t.Fatal(err)
	}
	if len(d) != 1 {
		t.Fatalf("want exactly 1 drift (port), got %d: %+v", len(d), d)
	}
	if d[0].Field != keyPort || d[0].RepoValue != "8080" || d[0].LiveValue != "3000" {
		t.Fatalf("port drift wrong: %+v", d[0])
	}
}

func TestDrift_StaticIgnoresUnsupportedFields(t *testing.T) {
	live := json.RawMessage(`{"deploy_strategy":"recreate","mode":"static"}`)
	// port declared but unsupported for static -> ignored; strategy differs -> drift.
	spec := Spec{Version: 1, Deploy: DeploySpec{Port: intp(8080), DeployStrategy: strp("blue-green")}}
	d, err := Drift(spec, live, SourceStatic)
	if err != nil {
		t.Fatal(err)
	}
	if len(d) != 1 || d[0].Field != keyDeployStrategy {
		t.Fatalf("static should only drift on deploy_strategy: %+v", d)
	}
}

func TestDrift_UnsetLiveValue(t *testing.T) {
	spec := Spec{Version: 1, Deploy: DeploySpec{Healthcheck: strp("/up")}}
	d, err := Drift(spec, json.RawMessage(`{}`), SourceDockerfile)
	if err != nil {
		t.Fatal(err)
	}
	if len(d) != 1 || d[0].RepoValue != "/up" || d[0].LiveValue != "(unset)" {
		t.Fatalf("unset live should render as (unset): %+v", d)
	}
}

func TestRedact_StripsToken(t *testing.T) {
	msg := redact(errors.New("execute request: token ghp_SECRET rejected"), "ghp_SECRET")
	if strings.Contains(msg, "ghp_SECRET") {
		t.Fatalf("token leaked: %s", msg)
	}
	if !strings.Contains(msg, "[redacted]") {
		t.Fatalf("expected redaction marker: %s", msg)
	}
	if redact(nil, "x") != "" {
		t.Fatalf("nil error should redact to empty string")
	}
}
