package checkpoint

import (
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

func TestNewIdentityNormalizesOrderAndEmptyValues(t *testing.T) {
	base := representativeIdentityInput()
	identity, err := NewIdentity(base)
	if err != nil {
		t.Fatal(err)
	}

	for _, tt := range []struct {
		name   string
		mutate func(*IdentityInput)
	}{
		{"selected lanes", func(v *IdentityInput) { v.SelectedLanes = []string{"lane-a", "lane-b"} }},
		{"runtime fingerprints", func(v *IdentityInput) {
			v.RuntimeOverrides = []Fingerprint{{Name: "model", Value: "large"}, {Name: "timeout", Value: "30s"}}
		}},
		{"references", func(v *IdentityInput) {
			v.References = []artifacts.ReferenceProvenance{v.References[1], v.References[0]}
		}},
		{"provenance fingerprints", func(v *IdentityInput) {
			v.ProvenanceFingerprints = []Fingerprint{{Name: "source", Value: "v2"}, {Name: "runner", Value: "v1"}}
		}},
		{"resolved lanes", func(v *IdentityInput) {
			steps := append([]pipeline.ResolvedPipelineStep(nil), v.Pipeline.Steps...)
			steps[0].ArtifactLanes = []pipeline.ResolvedArtifactLane{steps[0].ArtifactLanes[1], steps[0].ArtifactLanes[0]}
			v.Pipeline.Steps = steps
		}},
	} {
		t.Run(tt.name, func(t *testing.T) {
			changed := cloneIdentityInput(base)
			tt.mutate(&changed)
			got, err := NewIdentity(changed)
			if err != nil {
				t.Fatal(err)
			}
			if tt.name == "resolved lanes" {
				if reflect.DeepEqual(identity, got) {
					t.Fatal("reordered topology did not change checkpoint identity")
				}
				return
			}
			if !reflect.DeepEqual(identity, got) {
				t.Fatalf("reordered identity differs:\nbase=%#v\ngot=%#v", identity, got)
			}
		})
	}

	t.Run("duplicates and blanks are ignored", func(t *testing.T) {
		changed := base
		changed.SelectedLanes = []string{" ", "lane-b", "lane-a", "lane-a", ""}
		changed.RuntimeOverrides = append(changed.RuntimeOverrides, Fingerprint{}, Fingerprint{Name: " ", Value: "ignored"}, Fingerprint{Name: "timeout", Value: "30s"})
		changed.ProvenanceFingerprints = append(changed.ProvenanceFingerprints, Fingerprint{}, Fingerprint{Name: "", Value: "ignored"})

		got, err := NewIdentity(changed)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(identity, got) {
			t.Fatalf("empty or duplicate values changed identity:\nbase=%#v\ngot=%#v", identity, got)
		}
	})
}

func TestNewIdentityChangesForMeaningfulInputs(t *testing.T) {
	base := representativeIdentityInput()
	original, err := NewIdentity(base)
	if err != nil {
		t.Fatal(err)
	}

	cases := map[string]func(*IdentityInput){
		"pipeline id":        func(v *IdentityInput) { v.Pipeline.ID = "another-pipeline" },
		"pipeline digest":    func(v *IdentityInput) { v.Pipeline.Digest = "sha256:pipeline-digest-2" },
		"input key":          func(v *IdentityInput) { v.InputKey = "another-input" },
		"raw input digest":   func(v *IdentityInput) { v.RawInputDigest = "sha256:raw-input-2" },
		"source digest":      func(v *IdentityInput) { v.SourceDigest = "sha256:source-2" },
		"selected lanes":     func(v *IdentityInput) { v.SelectedLanes = []string{"lane-a"} },
		"runtime override":   func(v *IdentityInput) { v.RuntimeOverrides[0].Value = "60s" },
		"reference digest":   func(v *IdentityInput) { v.References[0].Digest = "sha256:reference-2" },
		"reference identity": func(v *IdentityInput) { v.References[0].OriginURI = "file:///other-reference" },
		"provenance":         func(v *IdentityInput) { v.ProvenanceFingerprints[0].Value = "v3" },
	}

	for name, mutate := range cases {
		t.Run(name, func(t *testing.T) {
			changed := cloneIdentityInput(base)
			mutate(&changed)
			got, err := NewIdentity(changed)
			if err != nil {
				t.Fatal(err)
			}
			if got.Digest == original.Digest {
				t.Fatalf("meaningful %s input did not change digest %q", name, got.Digest)
			}
		})
	}
}

func TestNewIdentityUsesResolvedInputWhenKeyIsOmitted(t *testing.T) {
	input := representativeIdentityInput()
	input.InputKey = ""

	identity, err := NewIdentity(input)
	if err != nil {
		t.Fatal(err)
	}
	if identity.InputKey != input.Pipeline.Input.Module {
		t.Fatalf("input key = %q, want resolved module %q", identity.InputKey, input.Pipeline.Input.Module)
	}
}

func TestNewIdentityRejectsMissingRequiredInputs(t *testing.T) {
	cases := []struct {
		name   string
		mutate func(*IdentityInput)
		want   string
	}{
		{"pipeline id", func(v *IdentityInput) { v.Pipeline.ID = "" }, "pipeline id"},
		{"pipeline digest", func(v *IdentityInput) { v.Pipeline.Digest = "" }, "pipeline digest"},
		{"input key", func(v *IdentityInput) { v.InputKey = ""; v.Pipeline.Input = pipeline.Binding("") }, "input key"},
		{"input digests", func(v *IdentityInput) { v.RawInputDigest = ""; v.SourceDigest = "" }, "raw input digest or source digest"},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			input := representativeIdentityInput()
			tt.mutate(&input)
			_, err := NewIdentity(input)
			if err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("error = %v, want category containing %q", err, tt.want)
			}
		})
	}
}

func TestIdentityRelativePathIsDeterministicAndConfined(t *testing.T) {
	identity, err := NewIdentity(representativeIdentityInput())
	if err != nil {
		t.Fatal(err)
	}

	first, err := identity.RelativePath()
	if err != nil {
		t.Fatal(err)
	}
	second, err := identity.RelativePath()
	if err != nil {
		t.Fatal(err)
	}
	if first != second {
		t.Fatalf("relative path is not deterministic: %q != %q", first, second)
	}
	if filepath.IsAbs(first) || filepath.Clean(first) != first || filepath.ToSlash(first) != first {
		t.Fatalf("path is not a clean relative slash-separated path: %q", first)
	}
	if strings.Contains(first, "../") || strings.HasPrefix(first, "../") || strings.Contains(first, `\\`) {
		t.Fatalf("path escapes its root: %q", first)
	}
	parts := strings.Split(first, "/")
	if len(parts) != 4 || parts[0] != "pipeline" || !strings.HasPrefix(parts[1], "input-") {
		t.Fatalf("path does not contain the documented identity hierarchy: %q", first)
	}
	if !strings.Contains(parts[1], "source-digest") || !strings.Contains(parts[2], "pipeline-digest") || parts[3] == "" {
		t.Fatalf("path omits digest-derived hierarchy: %q", first)
	}
}

func representativeIdentityInput() IdentityInput {
	return IdentityInput{
		Pipeline: pipeline.ResolvedPipeline{
			ID:     "pipeline",
			Digest: "sha256:pipeline-digest-000000000000",
			Input:  pipeline.Binding("input"),
			Steps: []pipeline.ResolvedPipelineStep{{
				ID: "default",
				ArtifactLanes: []pipeline.ResolvedArtifactLane{
					{ID: "lane-b"},
					{ID: "lane-a"},
				},
			}},
		},
		InputKey:         "input",
		RawInputDigest:   "sha256:raw-input-000000000000",
		SourceDigest:     "sha256:source-digest-000000000000",
		SelectedLanes:    []string{"lane-b", "lane-a"},
		RuntimeOverrides: []Fingerprint{{Name: "timeout", Value: "30s"}, {Name: "model", Value: "large"}},
		References: []artifacts.ReferenceProvenance{
			{Stage: "chunk", SlotName: "glossary", OriginURI: "file:///glossary", Digest: "sha256:reference-1"},
			{Stage: "extract", LaneID: "lane-a", SlotName: "party", OriginURI: "file:///party", Digest: "sha256:reference-2"},
		},
		ProvenanceFingerprints: []Fingerprint{{Name: "runner", Value: "v1"}, {Name: "source", Value: "v2"}},
	}
}

func cloneIdentityInput(input IdentityInput) IdentityInput {
	input.SelectedLanes = append([]string(nil), input.SelectedLanes...)
	input.RuntimeOverrides = append([]Fingerprint(nil), input.RuntimeOverrides...)
	input.References = append([]artifacts.ReferenceProvenance(nil), input.References...)
	input.ProvenanceFingerprints = append([]Fingerprint(nil), input.ProvenanceFingerprints...)
	input.Pipeline.Steps[0].ArtifactLanes = append([]pipeline.ResolvedArtifactLane(nil), input.Pipeline.Steps[0].ArtifactLanes...)
	return input
}
