package config

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

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

func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) {
	bindings := map[string]pipeline.ModuleBinding{}
	for _, name := range []string{
		"input", "chunk", "output", "extract", "merge", "normalize",
		"resolved-validator", "lane-validator",
	} {
		bindings[name] = redactionTestBinding(name)
	}

	resolved := pipeline.ResolvedPipeline{
		ID:                  "redaction-test",
		Digest:              "sha256:safe-digest",
		Input:               bindings["input"],
		InputExecutionClass: contracts.ExecutionClassDeterministic,
		Chunk:               bindings["chunk"],
		ChunkExecutionClass: contracts.ExecutionClassLLMBacked,
		ChunkReferences:     redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"),
		Steps: []pipeline.ResolvedPipelineStep{{
			ID: "default",
			ArtifactLanes: []pipeline.ResolvedArtifactLane{{
				ID:                      "safe-lane",
				ArtifactKind:            "safe/artifact",
				Extract:                 bindings["extract"],
				ExtractExecutionClass:   contracts.ExecutionClassLLMBacked,
				Merge:                   bindings["merge"],
				MergeExecutionClass:     contracts.ExecutionClassDeterministic,
				Normalize:               bindings["normalize"],
				NormalizeExecutionClass: contracts.ExecutionClassLLMBacked,
				Validators:              []pipeline.ModuleBinding{bindings["lane-validator"]},
				ExtractReferences:       redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"),
				MergeReferences:         redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"),
				NormalizeReferences:     redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"),
			}},
		}},
		ValidatorChains: []pipeline.ResolvedValidatorChain{{
			Stage:     pipeline.StageExtract,
			LaneID:    "safe-lane",
			ModuleKey: "safe-extract-owner",
			Validators: []pipeline.ResolvedValidator{{
				Binding:        bindings["resolved-validator"],
				ExecutionClass: contracts.ExecutionClassDeterministic,
				Target:         pipeline.ValidatorTargetTyped,
				ArtifactKind:   "safe/artifact",
			}},
		}},
		Output:               bindings["output"],
		OutputExecutionClass: contracts.ExecutionClassDeterministic,
	}
	effective := EffectiveConfig{
		Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
			"redaction-test": {Input: bindings["input"]},
		}},
		PipelineID:       "redaction-test",
		ResolvedPipeline: resolved,
	}

	payload := effective.RedactedResolvedPipelinePayload()
	encoded, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	text := string(encoded)
	for name := range bindings {
		for _, forbidden := range []string{name + "-secret", name + "-nested-secret"} {
			if strings.Contains(text, forbidden) {
				t.Fatalf("resolved pipeline summary contains %q: %s", forbidden, text)
			}
		}
		if !strings.Contains(text, name+"-safe") {
			t.Fatalf("resolved pipeline summary does not retain safe option for %q: %s", name, text)
		}
	}
	for _, content := range []string{
		"chunk-reference-content", "extract-reference-content",
		"merge-reference-content", "normalize-reference-content",
	} {
		if strings.Contains(text, content) {
			t.Fatalf("resolved pipeline summary contains materialized reference content %q", content)
		}
	}
	for _, safe := range []string{"[REDACTED]", "safe-reference-path", "safe-binding-source"} {
		if !strings.Contains(text, safe) {
			t.Fatalf("resolved pipeline summary does not retain %q: %s", safe, text)
		}
	}
	for _, executionClass := range []string{"input_execution_class\":\"deterministic", "chunk_execution_class\":\"llm_backed", "extract_execution_class\":\"llm_backed", "merge_execution_class\":\"deterministic", "normalize_execution_class\":\"llm_backed", "output_execution_class\":\"deterministic"} {
		if !strings.Contains(text, executionClass) {
			t.Fatalf("resolved pipeline summary does not retain %q: %s", executionClass, text)
		}
	}

	payload.Input.Options["safe"] = "mutated"
	nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any)
	nested["neighbor"] = "mutated"
	payload.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content[0] = 'X'
	payload.ValidatorChains[0].Validators[0].Binding.Options["safe"] = "mutated"

	assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "input")
	assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "input")
	assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.ValidatorChains[0].Validators[0].Binding, "resolved-validator")
	if got := string(effective.ResolvedPipeline.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content); got != "chunk-reference-content" {
		t.Fatalf("source reference content mutated through redacted payload: %q", got)
	}
}

func TestRedactedEffectiveConfigPayloadDoesNotAliasSource(t *testing.T) {
	binding := redactionTestBinding("effective")
	effective := EffectiveConfig{
		Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
			"redaction-test": {Input: binding},
		}},
		ResolvedPipeline: pipeline.ResolvedPipeline{Input: binding},
	}

	payload := effective.RedactedSummaryPayload().(EffectiveConfig)
	payload.Config.Pipelines["redaction-test"].Input.Options["safe"] = "mutated"
	payload.ResolvedPipeline.Input.Options["safe"] = "mutated"

	assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "effective")
	assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "effective")
}

func TestRedactedSummaryPayloadsCoverEveryEffectiveConfigBinding(t *testing.T) {
	bindings := map[string]pipeline.ModuleBinding{}
	for _, name := range []string{"input", "chunk", "output", "extract", "merge", "normalize", "lane-validator"} {
		bindings[name] = redactionTestBinding("summary-" + name)
	}
	effective := EffectiveConfig{
		Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
			"redaction-test": {
				Input:  bindings["input"],
				Chunk:  bindings["chunk"],
				Output: bindings["output"],
				Artifacts: map[string]pipeline.ArtifactLaneProfile{
					"safe-lane": {
						Extract:    bindings["extract"],
						Merge:      bindings["merge"],
						Normalize:  bindings["normalize"],
						Validators: []pipeline.ModuleBinding{bindings["lane-validator"]},
					},
				},
			},
		}},
		ResolvedPipeline: pipeline.ResolvedPipeline{
			Input:  bindings["input"],
			Chunk:  bindings["chunk"],
			Output: bindings["output"],
			Steps: []pipeline.ResolvedPipelineStep{{
				ID: "default",
				ArtifactLanes: []pipeline.ResolvedArtifactLane{{
					ID:         "safe-lane",
					Extract:    bindings["extract"],
					Merge:      bindings["merge"],
					Normalize:  bindings["normalize"],
					Validators: []pipeline.ModuleBinding{bindings["lane-validator"]},
				}},
			}},
		},
	}

	payload, ok := effective.RedactedSummaryPayload().(EffectiveConfig)
	if !ok {
		t.Fatal("RedactedSummaryPayload() returned an unexpected type")
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	text := string(encoded)
	for name := range bindings {
		if strings.Contains(text, "summary-"+name+"-secret") || strings.Contains(text, "summary-"+name+"-nested-secret") {
			t.Fatalf("summary payload contains sensitive option for %q: %s", name, text)
		}
		if !strings.Contains(text, "summary-"+name+"-safe") {
			t.Fatalf("summary payload omitted safe option for %q: %s", name, text)
		}
	}
	if !strings.Contains(text, "[REDACTED]") {
		t.Fatalf("summary payload contains no redaction marker: %s", text)
	}
}

func TestRedactedResolvedPipelinePayloadHandlesTypedOptionContainers(t *testing.T) {
	type optionMap map[string]string
	type optionList []optionMap

	typed := optionList{{
		"api_key": "typed-container-secret",
		"safe":    "typed-container-safe",
	}}
	effective := EffectiveConfig{ResolvedPipeline: pipeline.ResolvedPipeline{
		Input: pipeline.ModuleBinding{Options: map[string]any{"nested": typed}},
	}}

	payload := effective.RedactedResolvedPipelinePayload()
	nested, ok := payload.Input.Options["nested"].([]any)
	if !ok || len(nested) != 1 {
		t.Fatalf("redacted typed list = %#v", payload.Input.Options["nested"])
	}
	item, ok := nested[0].(map[string]any)
	if !ok {
		t.Fatalf("redacted typed map = %#v", nested[0])
	}
	if got := item["api_key"]; got != "[REDACTED]" {
		t.Fatalf("redacted api_key = %v", got)
	}
	if got := item["safe"]; got != "typed-container-safe" {
		t.Fatalf("safe option = %v", got)
	}

	item["safe"] = "mutated"
	if got := typed[0]["safe"]; got != "typed-container-safe" {
		t.Fatalf("source typed map mutated through redacted payload: %q", got)
	}
}

func TestRedactedEffectiveConfigPayloadOwnsValidationPolicies(t *testing.T) {
	semantic := pipeline.SemanticRejectionRejectOutput
	validator := pipeline.ValidatorFailureFailRun
	configured := &pipeline.ValidationPolicyOverride{SemanticRejection: &semantic, ValidatorFailure: &validator}
	effective := EffectiveConfig{
		Config: Config{Pipelines: map[string]pipeline.PipelineProfile{
			"main": {ValidationPolicy: configured},
		}},
		ResolvedPipeline: pipeline.ResolvedPipeline{
			ConfiguredValidationPolicy: configured,
			ChunkValidationPolicy: pipeline.ValidationPolicy{
				ProducerStructuralFailure: pipeline.ProducerStructuralFailureFailRun,
				SemanticRejection:         pipeline.SemanticRejectionRejectOutput,
				ValidatorFailure:          pipeline.ValidatorFailureFailRun,
			},
		},
	}

	payload := effective.RedactedSummaryPayload().(EffectiveConfig)
	encoded, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(encoded), `"configured_validation_policy":{"semantic_rejection":"reject_output","validator_failure":"fail_run"}`) || !strings.Contains(string(encoded), `"chunk_validation_policy":{"producer_structural_failure":"fail_run","semantic_rejection":"reject_output","validator_failure":"fail_run"}`) {
		t.Fatalf("redacted payload omitted validation policy: %s", encoded)
	}
	*payload.Config.Pipelines["main"].ValidationPolicy.SemanticRejection = pipeline.SemanticRejectionFailRun
	*payload.ResolvedPipeline.ConfiguredValidationPolicy.ValidatorFailure = pipeline.ValidatorFailureWarnContinue
	if *effective.Config.Pipelines["main"].ValidationPolicy.SemanticRejection != pipeline.SemanticRejectionRejectOutput || *effective.ResolvedPipeline.ConfiguredValidationPolicy.ValidatorFailure != pipeline.ValidatorFailureFailRun {
		t.Fatal("redacted payload aliases validation policy")
	}
}

func redactionTestBinding(name string) pipeline.ModuleBinding {
	return pipeline.ModuleBinding{
		Module: "safe-" + name,
		Options: map[string]any{
			"api_key": name + "-secret",
			"safe":    name + "-safe",
			"nested": []any{[]any{map[string]any{
				"password": name + "-nested-secret",
				"neighbor": name + "-nested-safe",
			}}},
		},
	}
}

func redactionTestReferenceTarget(stage pipeline.ModuleStage, laneID, content string) pipeline.ResolvedReferenceTarget {
	return pipeline.ResolvedReferenceTarget{
		Stage:  stage,
		LaneID: laneID,
		Module: "safe-reference-module",
		Bindings: []pipeline.ReferenceBinding{{
			Stage:         stage,
			LaneID:        laneID,
			SlotName:      "safe-slot",
			Source:        "safe-reference-path",
			BindingSource: "safe-binding-source",
		}},
		ReferenceSet: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
			"safe-slot": {
				Slot: contracts.ReferenceSlot{Name: "safe-slot"},
				Items: []contracts.ReferenceItem{{
					SlotName:      "safe-slot",
					Content:       []byte(content),
					Digest:        "sha256:safe-reference-digest",
					BindingSource: "safe-binding-source",
				}},
			},
		}},
	}
}

func assertRedactionTestBindingUnchanged(t *testing.T, binding pipeline.ModuleBinding, name string) {
	t.Helper()
	if got := binding.Options["safe"]; got != name+"-safe" {
		t.Fatalf("source safe option = %v, want %q", got, name+"-safe")
	}
	nested := binding.Options["nested"].([]any)[0].([]any)[0].(map[string]any)
	if got := nested["neighbor"]; got != name+"-nested-safe" {
		t.Fatalf("source nested safe option = %v, want %q", got, name+"-nested-safe")
	}
}
