package pipeline

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

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

type reuseLineageCheckpointSpy struct {
	CheckpointLoader
	CheckpointRecorder
	loads  map[string]int
	writes map[string]int
	forbid map[string]struct{}
}

func newReuseLineageCheckpointSpy() *reuseLineageCheckpointSpy {
	return &reuseLineageCheckpointSpy{
		CheckpointLoader:   NoopCheckpointLoader(),
		CheckpointRecorder: NoopCheckpointRecorder(),
		loads:              make(map[string]int),
		writes:             make(map[string]int),
		forbid:             make(map[string]struct{}),
	}
}

func (s *reuseLineageCheckpointSpy) Enabled() bool { return true }

func (s *reuseLineageCheckpointSpy) load(stage, laneID string) {
	key := stage + "/" + laneID
	if _, forbidden := s.forbid[key]; forbidden {
		panic("checkpoint lookup crossed validation-incomplete lineage: " + key)
	}
	s.loads[key]++
}

func (s *reuseLineageCheckpointSpy) write(stage, action, laneID string) {
	s.writes[stage+"/"+action+"/"+laneID]++
}

func (s *reuseLineageCheckpointSpy) Extract(laneID, _ string, _ []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision) {
	s.load("extract", laneID)
	return ExtractCheckpoint{}, NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
}

func (s *reuseLineageCheckpointSpy) Merge(laneID, _ string, _ []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision) {
	s.load("merge", laneID)
	return MergeCheckpoint{}, NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
}

func (s *reuseLineageCheckpointSpy) Normalize(laneID, _ string, _ []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision) {
	s.load("normalize", laneID)
	return NormalizeCheckpoint{}, NewCheckpointDecision(CheckpointDecisionExecuted, CheckpointReasonMissing)
}

func (s *reuseLineageCheckpointSpy) ExtractRunning(laneID, _ string, _ []CheckpointFingerprint) error {
	s.write("extract", "running", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) ExtractSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ []CheckpointArtifact, _ []contracts.RejectedOutput) error {
	s.write("extract", "succeeded", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) ExtractFailed(laneID, _ string, _ []CheckpointFingerprint, _ error) error {
	s.write("extract", "failed", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) MergeRunning(laneID, _ string, _ []CheckpointFingerprint) error {
	s.write("merge", "running", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) MergeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact) error {
	s.write("merge", "succeeded", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) MergeRejected(laneID, _ string, _ []CheckpointFingerprint, _ contracts.RejectedOutput) error {
	s.write("merge", "rejected", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) MergeFailed(laneID, _ string, _ []CheckpointFingerprint, _ error) error {
	s.write("merge", "failed", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) NormalizeRunning(laneID, _ string, _ []CheckpointFingerprint) error {
	s.write("normalize", "running", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) NormalizeSucceeded(laneID, _ string, _ []CheckpointFingerprint, _ CheckpointArtifact) error {
	s.write("normalize", "succeeded", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) NormalizeRejected(laneID, _ string, _ []CheckpointFingerprint, _ contracts.RejectedOutput) error {
	s.write("normalize", "rejected", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) NormalizeFailed(laneID, _ string, _ []CheckpointFingerprint, _ error) error {
	s.write("normalize", "failed", laneID)
	return nil
}

func (s *reuseLineageCheckpointSpy) writesFor(stage, laneID string) int {
	total := 0
	needle := stage + "/"
	suffix := "/" + laneID
	for key, count := range s.writes {
		if strings.HasPrefix(key, needle) && strings.HasSuffix(key, suffix) {
			total += count
		}
	}
	return total
}

func unavailableTypedValidator(kind contracts.ArtifactKind) preparedValidator {
	return preparedValidator{
		resolved: ResolvedValidator{Binding: Binding("typed/check"), Target: ValidatorTargetTyped, ArtifactKind: kind},
		typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
			return contracts.ValidationResult{}, fmt.Errorf("validator unavailable")
		},
	}
}

func TestValidationIncompleteExtractDisablesDownstreamCheckpointIO(t *testing.T) {
	prepared := preparedAttemptDebugPipeline(t)
	lane := &prepared.Steps[0].lanes[0]
	lane.resolved.ExtractValidationPolicy.ValidatorFailure = ValidatorFailureWarnContinue
	installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
		return erasedTypedResult{Value: typedValueForLane(0, request.Chunk.Index)}, nil
	})
	lane.extractValidators.validators[0].typedValidate = func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
		return contracts.ValidationResult{}, fmt.Errorf("validator unavailable")
	}
	spy := newReuseLineageCheckpointSpy()

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoint: spy, Checkpoints: spy})
	if err != nil {
		t.Fatalf("Run() error = %v", err)
	}
	if len(output.NormalizeOutputs) != 1 {
		t.Fatalf("normalize outputs = %d, want current-run handoff", len(output.NormalizeOutputs))
	}
	if spy.loads["merge/notes"] != 0 || spy.loads["normalize/notes"] != 0 {
		t.Fatalf("downstream loads = %#v, want none", spy.loads)
	}
	if spy.writesFor("merge", "notes") != 0 || spy.writesFor("normalize", "notes") != 0 {
		t.Fatalf("downstream writes = %#v, want none", spy.writes)
	}
	if spy.writes["extract/succeeded/notes"] != 0 {
		t.Fatalf("extract writes = %#v, want no reusable success", spy.writes)
	}
}

func TestValidationIncompleteMergeDisablesNormalizeCheckpointIO(t *testing.T) {
	prepared := preparedAttemptDebugPipeline(t)
	lane := &prepared.Steps[0].lanes[0]
	lane.resolved.MergeValidationPolicy.ValidatorFailure = ValidatorFailureWarnContinue
	installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
		return erasedTypedResult{Value: typedValueForLane(0, request.Chunk.Index)}, nil
	})
	lane.mergeValidators.validators = []preparedValidator{unavailableTypedValidator(lane.resolved.ArtifactKind)}
	spy := newReuseLineageCheckpointSpy()

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoint: spy, Checkpoints: spy})
	if err != nil {
		t.Fatalf("Run() error = %v", err)
	}
	if len(output.NormalizeOutputs) != 1 {
		t.Fatalf("normalize outputs = %d, want current-run handoff", len(output.NormalizeOutputs))
	}
	if spy.loads["merge/notes"] != 1 || spy.loads["normalize/notes"] != 0 {
		t.Fatalf("loads = %#v, want merge lookup only", spy.loads)
	}
	if spy.writes["merge/succeeded/notes"] != 0 || spy.writesFor("normalize", "notes") != 0 {
		t.Fatalf("writes = %#v, want no reusable merge or normalize state", spy.writes)
	}
}

func TestValidationIncompleteNormalizeIsNotPublished(t *testing.T) {
	prepared := preparedAttemptDebugPipeline(t)
	lane := &prepared.Steps[0].lanes[0]
	lane.resolved.NormalizeValidationPolicy.ValidatorFailure = ValidatorFailureWarnContinue
	installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
		return erasedTypedResult{Value: typedValueForLane(0, request.Chunk.Index)}, nil
	})
	lane.normalizeValidators.validators = []preparedValidator{unavailableTypedValidator(lane.resolved.ArtifactKind)}
	spy := newReuseLineageCheckpointSpy()

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoint: spy, Checkpoints: spy})
	if err != nil {
		t.Fatalf("Run() error = %v", err)
	}
	if len(output.NormalizeOutputs) != 1 || spy.writes["normalize/succeeded/notes"] != 0 {
		t.Fatalf("output/writes = %d / %#v, want in-memory output without normalize publication", len(output.NormalizeOutputs), spy.writes)
	}
}

func TestGeneratedReferenceFromIncompleteValidationDisablesDependentCheckpointIO(t *testing.T) {
	input, _, _ := handoffFixture(t, codecNotes{Items: []string{"producer"}})
	prepared := input.Prepared
	producer := &prepared.Steps[0].lanes[0]
	consumer := &prepared.Steps[1].lanes[0]
	producer.resolved.NormalizeValidationPolicy.ValidatorFailure = ValidatorFailureWarnContinue
	producer.normalizeValidators.validators = []preparedValidator{unavailableTypedValidator(producer.resolved.ArtifactKind)}
	referenceItems := 0
	consumer.typed.extract = func(_ context.Context, _ any, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
		referenceItems = len(request.References.Slots["producer-output"].Items)
		return erasedTypedResult{Value: codecScore{Value: 1}}, nil
	}
	spy := newReuseLineageCheckpointSpy()
	for _, stage := range []string{"extract", "merge", "normalize"} {
		spy.forbid[stage+"/score"] = struct{}{}
	}

	output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoint: spy, Checkpoints: spy})
	if err != nil {
		t.Fatalf("Run() error = %v", err)
	}
	if referenceItems != 1 || len(output.NormalizeOutputs) != 2 {
		t.Fatalf("current-run handoff = items %d outputs %d, want one generated item and two outputs", referenceItems, len(output.NormalizeOutputs))
	}
	for _, stage := range []string{"extract", "merge", "normalize"} {
		if spy.writesFor(stage, "score") != 0 {
			t.Fatalf("dependent writes = %#v, want none for %s", spy.writes, stage)
		}
	}
}
