package pipeline

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

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

type requiredCheckpointLoader struct {
	CheckpointLoader
	checkpoint ExtractCheckpoint
	decision   CheckpointDecision
}

func (l requiredCheckpointLoader) Enabled() bool { return true }
func (l requiredCheckpointLoader) Extract(string, string, []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision) {
	return l.checkpoint, l.decision
}
func (l requiredCheckpointLoader) AcceptedNormalize(string, string, string) (NormalizeCheckpoint, CheckpointDecision) {
	output := CheckpointArtifact{}
	if len(l.checkpoint.Outputs) > 0 {
		output = l.checkpoint.Outputs[0]
	}
	return NormalizeCheckpoint{Output: output}, l.decision
}

func TestRequiredCheckpointFailureRetainsDecision(t *testing.T) {
	tests := []struct {
		name       string
		decision   CheckpointDecision
		corrupt    bool
		wantCode   CheckpointReasonCode
		wantAction CheckpointDecisionCategory
	}{
		{"missing", NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing), false, CheckpointReasonMissing, CheckpointDecisionExecuted},
		{"corrupt", NewCheckpointDecision(CheckpointDecisionReused, CheckpointReasonReused), true, CheckpointReasonArtifactNotCanonical, CheckpointDecisionExecuted},
		{"incompatible", NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonArtifactCodecIncompatible), false, CheckpointReasonArtifactCodecIncompatible, CheckpointDecisionExecuted},
		{"dependency invalidated", NewCheckpointDecision(CheckpointDecisionDependencyInvalidated, CheckpointReasonDependencyMismatch), false, CheckpointReasonDependencyMismatch, CheckpointDecisionDependencyInvalidated},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			prepared := preparedConcurrentPipeline(t, 1)
			step := prepared.Steps[0]
			lane := step.lanes[0]
			doc := prepared.input.(*typedTestInput).doc
			checkpoint := ExtractCheckpoint{}
			if test.corrupt {
				stored, err := checkpointArtifact(lane.typed.codec, lane.resolved.ID, lane.resolved.Normalize.Module, doc.ID, codecNotes{Items: []string{"stored"}})
				if err != nil {
					t.Fatal(err)
				}
				stored.Artifact.Content = []byte(`{"items": ["stored"]}`)
				checkpoint.Outputs = []CheckpointArtifact{stored}
			}
			loader := requiredCheckpointLoader{CheckpointLoader: NoopCheckpointLoader(), checkpoint: checkpoint, decision: test.decision}
			policy := CheckpointExecutionPolicy{RequireReusableLanes: map[string]struct{}{CheckpointLaneKey(step.ID, lane.resolved.ID): {}}}
			output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoint: loader, CheckpointPolicy: policy})
			if err == nil || !strings.Contains(err.Error(), step.ID) || !strings.Contains(err.Error(), lane.resolved.ID) || !strings.Contains(err.Error(), string(test.wantCode)) {
				t.Fatalf("Run() error = %v, want step, lane, and reason code %q", err, test.wantCode)
			}
			var found bool
			for _, event := range output.CheckpointEvents {
				if event.Stage == string(StageNormalize) && event.StepID == step.ID && event.LaneID == lane.resolved.ID {
					found = true
					if event.Action != test.wantAction || event.ReasonCode != test.wantCode {
						t.Fatalf("checkpoint event = %#v, want action %q and reason %q", event, test.wantAction, test.wantCode)
					}
					if event.Detail == "" || event.Detail != event.Reason {
						t.Fatalf("checkpoint event detail is not code-owned compatibility text: %#v", event)
					}
				}
			}
			if !found {
				t.Fatalf("required checkpoint decision missing from %#v", output.CheckpointEvents)
			}
			var manifestFound bool
			for _, decision := range output.Manifest.CheckpointDecisions {
				if decision.Stage == string(StageNormalize) && decision.StepID == step.ID && decision.LaneID == lane.resolved.ID {
					manifestFound = decision.Category == string(test.wantAction) && decision.ReasonCode == string(test.wantCode)
				}
			}
			if !manifestFound {
				t.Fatalf("manifest checkpoint decision missing category %q and code %q: %#v", test.wantAction, test.wantCode, output.Manifest.CheckpointDecisions)
			}
			encoded, marshalErr := json.Marshal(output.CheckpointEvents)
			if marshalErr != nil || !strings.Contains(string(encoded), `"category":"`+string(test.wantAction)+`"`) || !strings.Contains(string(encoded), `"reason_code":"`+string(test.wantCode)+`"`) {
				t.Fatalf("checkpoint event JSON = %s, error = %v", encoded, marshalErr)
			}
		})
	}
}

func TestCheckpointDecisionDetailDoesNotCopyUnknownReasonCode(t *testing.T) {
	const sentinel = "opaque-sensitive-value-78421"
	decision := NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonCode(sentinel))
	if decision.Detail == "" || decision.Detail != decision.Reason {
		t.Fatalf("decision detail is not bounded compatibility text: %#v", decision)
	}
	if strings.Contains(decision.Detail, sentinel) || strings.Contains(decision.Reason, sentinel) {
		t.Fatalf("decision detail copied caller-controlled reason code: %#v", decision)
	}
}

func TestDecodeCheckpointArtifactRejectsIncompatibleCodecIdentityAndBytes(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec: %v", err)
	}
	codec, _, err := registry.entry("test/notes")
	if err != nil {
		t.Fatalf("entry: %v", err)
	}
	artifact, err := serializeArtifact(codec, codecNotes{Items: []string{"one"}}, false)
	if err != nil {
		t.Fatalf("serializeArtifact: %v", err)
	}
	base := CheckpointArtifact{Artifact: artifact, SchemaDigest: contracts.DigestArtifactSchema(artifact.Schema)}

	tests := []struct {
		name   string
		mutate func(*CheckpointArtifact)
		want   string
	}{
		{name: "missing kind", mutate: func(v *CheckpointArtifact) { v.Artifact.Kind = "" }, want: "artifact kind"},
		{name: "schema version", mutate: func(v *CheckpointArtifact) { v.Artifact.Schema.Version = "v999" }, want: "does not match codec schema"},
		{name: "schema digest", mutate: func(v *CheckpointArtifact) { v.SchemaDigest = "sha256:different" }, want: "schema digest"},
		{name: "media type", mutate: func(v *CheckpointArtifact) { v.Artifact.MediaType = "text/plain" }, want: "media type"},
		{name: "decode failure", mutate: func(v *CheckpointArtifact) { v.Artifact.Content = []byte(`{"items":[`) }, want: "unexpected EOF"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			stored := cloneCheckpointArtifact(base)
			test.mutate(&stored)
			if _, err := decodeCheckpointArtifact(codec, stored); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("decode error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestDecodeCanonicalCheckpointArtifactRejectsNonCanonicalBytes(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec: %v", err)
	}
	codec, _, err := registry.entry("test/notes")
	if err != nil {
		t.Fatalf("entry: %v", err)
	}
	artifact, err := serializeArtifact(codec, codecNotes{Items: []string{"one"}}, false)
	if err != nil {
		t.Fatalf("serializeArtifact: %v", err)
	}
	stored := CheckpointArtifact{Artifact: artifact, SchemaDigest: contracts.DigestArtifactSchema(artifact.Schema)}
	if _, _, err := decodeCanonicalCheckpointArtifact(codec, stored); err != nil {
		t.Fatalf("canonical artifact rejected: %v", err)
	}
	stored.Artifact.Content = []byte(`{"items": ["one"]}`)
	if _, _, err := decodeCanonicalCheckpointArtifact(codec, stored); err == nil {
		t.Fatal("non-canonical artifact was accepted")
	}
}
