package pipeline

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

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

type warningChunker struct {
	key   string
	plan  source.ChunkPlan
	calls int
}

func (c *warningChunker) Key() string                             { return c.key }
func (*warningChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (c *warningChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
	c.calls++
	return contracts.ChunkPlanResult{
		Plan:     source.CloneChunkPlan(c.plan),
		Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", c.calls), ReasonCode: "operation", Message: "operation warning"}},
	}, nil
}

type chunkValidationFunc struct {
	name     string
	validate func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error)
}

func (v chunkValidationFunc) Name() string { return v.name }
func (chunkValidationFunc) ExecutionClass() contracts.ExecutionClass {
	return contracts.ExecutionClassDeterministic
}
func (v chunkValidationFunc) Validate(ctx context.Context, request contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
	return v.validate(ctx, request)
}

func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
	for _, target := range []ModuleStage{StageChunk, StageExtract, StageMerge, StageNormalize} {
		t.Run(string(target), func(t *testing.T) {
			prepared := preparedAttemptDebugPipeline(t)
			lane := &prepared.Steps[0].lanes[0]
			attempts := 0
			first := func() contracts.ValidationResult {
				return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", attempts), ReasonCode: "validator", Message: "validator warning"}}}
			}
			reject := func() contracts.ValidationResult {
				return contracts.ValidationResult{Approved: false, ReasonCode: "rejected", Message: "rejected"}
			}
			debug := newCapturedDebugRecorder()
			recorder := &extractCaptureRecorder{CheckpointRecorder: NoopCheckpointRecorder()}

			switch target {
			case StageChunk:
				chunker := prepared.chunker.(*typedTestChunker)
				prepared.chunker = &warningChunker{key: prepared.resolved.Chunk.Module, plan: source.CloneChunkPlan(chunker.plan)}
				prepared.resolved.Chunk.Retries = 1
				prepared.chunkValidators.validators = []preparedValidator{
					{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetChunk}, chunk: chunkValidationFunc{name: "warning-approval", validate: func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
						return first(), nil
					}}},
					{resolved: ResolvedValidator{Binding: Binding("warning-rejection"), Target: ValidatorTargetChunk}, chunk: chunkValidationFunc{name: "warning-rejection", validate: func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
						return reject(), nil
					}}},
				}
				chunkerWithWarnings := prepared.chunker.(*warningChunker)
				first = func() contracts.ValidationResult {
					return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", chunkerWithWarnings.calls), ReasonCode: "validator", Message: "validator warning"}}}
				}
			case StageExtract:
				lane.resolved.Extract.Retries = 1
				installExtractOperation(prepared, 0, func(context.Context, contracts.TypedExtractionRequest) (erasedTypedResult, error) {
					attempts++
					return erasedTypedResult{Value: codecNotes{Items: []string{"extract"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
				})
				lane.extractValidators.validators = rejectionWarningTypedValidators(first, reject)
			case StageMerge:
				lane.resolved.Merge.Retries = 1
				lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
					attempts++
					return erasedTypedResult{Value: codecNotes{Items: []string{"merge"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
				}
				lane.mergeValidators.validators = rejectionWarningTypedValidators(first, reject)
			case StageNormalize:
				lane.resolved.Normalize.Retries = 1
				lane.typed.normalize = func(context.Context, any, contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
					attempts++
					return erasedTypedResult{Value: codecNotes{Items: []string{"normalize"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
				}
				lane.normalizeValidators.validators = rejectionWarningTypedValidators(first, reject)
			}

			output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoints: recorder, Debug: debug})
			if err != nil {
				t.Fatalf("Run() error = %v, want nil", err)
			}
			wantScopes := []string{"operation-2", "validator-2"}
			if got := rejectionWarningScopes(output.Warnings); !reflect.DeepEqual(got, wantScopes) {
				t.Fatalf("published warning scopes = %#v, want %#v", got, wantScopes)
			}
			if len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != 2 {
				t.Fatalf("rejections = %#v, want final rejection after two attempts", output.Rejected)
			}
			if target == StageExtract && !reflect.DeepEqual(rejectionWarningScopes(recorder.checkpoint.Warnings), wantScopes) {
				t.Fatalf("extract checkpoint warnings = %#v, want %#v", recorder.checkpoint.Warnings, wantScopes)
			}
			attemptPath := fmt.Sprintf("%s/notes/attempt-01.json", target)
			if target == StageChunk {
				attemptPath = "chunk/attempt-01.json"
			} else if target == StageExtract {
				attemptPath = "extract/notes/chunk-000001/attempt-01.json"
			}
			if !strings.Contains(string(debug.json[attemptPath]), "operation-1") || !strings.Contains(string(debug.json[attemptPath]), "validator-1") {
				t.Fatalf("first attempt debug = %s, want discarded warnings", debug.json[attemptPath])
			}
		})
	}
}

func rejectionWarningTypedValidators(first func() contracts.ValidationResult, reject func() contracts.ValidationResult) []preparedValidator {
	return []preparedValidator{
		{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
			return first(), nil
		}},
		{resolved: ResolvedValidator{Binding: Binding("warning-rejection"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
			return reject(), nil
		}},
	}
}

func rejectionWarningScopes(warnings []contracts.Warning) []string {
	scopes := make([]string, len(warnings))
	for index := range warnings {
		scopes[index] = warnings[index].Scope
	}
	return scopes
}
