package pipeline

import (
	"context"
	"fmt"
	"strings"
	"testing"

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

func TestRunnerCorrectsRejectedMergeAndNormalizeCandidates(t *testing.T) {
	for _, target := range []ModuleStage{StageMerge, StageNormalize} {
		t.Run(string(target), func(t *testing.T) {
			prepared := preparedAttemptDebugPipeline(t)
			lane := &prepared.Steps[0].lanes[0]
			var correction *contracts.SemanticCorrection
			validator := preparedValidator{
				resolved: ResolvedValidator{Binding: Binding("correction-check"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"},
				typedValidate: func(_ context.Context, _ any, target typedValidationTarget) (contracts.ValidationResult, error) {
					value := target.value.(codecNotes)
					if firstNote(value) == "invalid" {
						return contracts.ValidationResult{Approved: false, ReasonCode: "invalid_candidate", Message: "candidate needs correction", CorrectionGuidance: "produce the accepted value"}, nil
					}
					return contracts.ValidationResult{Approved: true}, nil
				},
			}
			operation := func(observed *contracts.SemanticCorrection) (erasedTypedResult, error) {
				if observed != nil {
					cloned, err := contracts.CloneSemanticCorrection(observed)
					if err != nil {
						return erasedTypedResult{}, err
					}
					correction = cloned
				}
				item, response := "invalid", `{"items":["invalid"]}`
				if observed != nil {
					item, response = "accepted", `{"items":["accepted"]}`
				}
				candidate, err := contracts.NewModelCandidate([]byte(response), contracts.CorrectionProtocolSingleResponseV1)
				if err != nil {
					return erasedTypedResult{}, err
				}
				return erasedTypedResult{Value: codecNotes{Items: []string{item}}, ModelCandidate: candidate}, nil
			}
			switch target {
			case StageMerge:
				lane.resolved.Merge.Retries = 1
				lane.mergeValidators.validators = []preparedValidator{validator}
				lane.typed.merge = func(_ context.Context, _ any, request contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
					return operation(request.Correction)
				}
			case StageNormalize:
				lane.resolved.Normalize.Retries = 1
				lane.normalizeValidators.validators = []preparedValidator{validator}
				lane.typed.normalize = func(_ context.Context, _ any, request contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
					return operation(request.Correction)
				}
			}

			output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input")})
			if err != nil {
				t.Fatalf("Run() error = %v, want nil", err)
			}
			if correction == nil || string(correction.AssistantResponse) != `{"items":["invalid"]}` || !strings.Contains(correction.UserGuidance, "produce the accepted value") || !strings.Contains(correction.UserGuidance, "complete corrected replacement") || strings.Contains(correction.UserGuidance, "invalid_candidate") || strings.Contains(correction.UserGuidance, "candidate needs correction") {
				t.Fatalf("%s correction = %#v, want exact rejected response and semantic replacement guidance only", target, correction)
			}
			if len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 {
				t.Fatalf("run output = %#v, want corrected accepted output", output)
			}
		})
	}
}

func TestRunnerRejectsDeterministicMergeCandidateWithoutCorrection(t *testing.T) {
	prepared := preparedAttemptDebugPipeline(t)
	lane := &prepared.Steps[0].lanes[0]
	lane.resolved.Merge.Retries = 1
	lane.resolved.MergeValidationPolicy.SemanticRejection = SemanticRejectionRejectOutput
	calls := 0
	lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
		calls++
		return erasedTypedResult{Value: codecNotes{Items: []string{"invalid"}}}, nil
	}
	lane.mergeValidators.validators = []preparedValidator{{
		resolved: ResolvedValidator{Binding: Binding("reject"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"},
		typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
			return contracts.ValidationResult{Approved: false, ReasonCode: "invalid_candidate", Message: "not accepted", CorrectionGuidance: "return an acceptable candidate"}, nil
		},
	}}

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input")})
	if err != nil {
		t.Fatalf("Run() error = %v, want non-fatal rejection", err)
	}
	if calls != 1 || len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != 1 {
		t.Fatalf("merge calls = %d output = %#v, want one deterministic terminal rejection", calls, output)
	}
}

func TestRunnerContinuesNormalizeAfterValidatorFailure(t *testing.T) {
	prepared := preparedAttemptDebugPipeline(t)
	lane := &prepared.Steps[0].lanes[0]
	lane.resolved.NormalizeValidationPolicy.ValidatorFailure = ValidatorFailureWarnContinue
	lane.normalizeValidators.validators = []preparedValidator{{
		resolved: ResolvedValidator{Binding: Binding("unavailable"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"},
		typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
			return contracts.ValidationResult{}, fmt.Errorf("validator unavailable")
		},
	}}
	checkpoints := &candidateCheckpointRecorder{CheckpointRecorder: NoopCheckpointRecorder()}

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoints: checkpoints})
	if err != nil {
		t.Fatalf("Run() error = %v, want incomplete accepted output", err)
	}
	if len(output.NormalizeOutputs) != 1 || len(output.Rejected) != 0 || checkpoints.normalizeSucceeded != 0 {
		t.Fatalf("run output = %#v checkpoints = %#v, want non-checkpointed incomplete normalized output", output, checkpoints)
	}
	if output.Manifest.ValidationStatus != "incomplete" || !containsValidationSummary(output.Manifest.ValidationSummaries, "incomplete") {
		t.Fatalf("manifest validation = %#v, want incomplete normalization provenance", output.Manifest)
	}
	if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != "validator_execution_incomplete" {
		t.Fatalf("diagnostics = %#v, want validation-incomplete group", output.Diagnostics)
	}
}
