package applicationresultv1

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestSemanticMappingRevisionCoversEveryPublishedShape(t *testing.T) {
	if SemanticMappingRevision != 1 {
		t.Fatalf("SemanticMappingRevision = %d; update every mapper before changing this test", SemanticMappingRevision)
	}
	values := []interface{ ApplicationResultSemanticMappingRevision() uint32 }{
		ExecutionRef{}, ApplicationResult{}, GetResultRequest{}, GetResultResponse{},
	}
	for _, value := range values {
		if got := value.ApplicationResultSemanticMappingRevision(); got != SemanticMappingRevision {
			t.Fatalf("mapping revision = %d", got)
		}
	}
}

func TestExecutionRefDigestHasFrozenV1Grammar(t *testing.T) {
	ref := goldenExecutionRef()
	digest, err := ExecutionRefDigest(ref)
	if err != nil {
		t.Fatalf("ExecutionRefDigest() error = %v", err)
	}
	const want = "sha256:6b25e1c2856eb232ddff9ba0d4f2c0eb49bc04fb5260e8f62a7ad3c6c6ec94d0"
	if digest != want {
		t.Fatalf("ExecutionRefDigest() = %q, want %q", digest, want)
	}

	changes := []func(*ExecutionRef){
		func(value *ExecutionRef) { value.ExecutionID = "execution-2" },
		func(value *ExecutionRef) { value.PackGenerationID = "pack-generation-2" },
		func(value *ExecutionRef) { value.ServingEpochDigest = digestOf("3") },
		func(value *ExecutionRef) { value.DefinitionRef = "flow.connector.other" },
		func(value *ExecutionRef) { value.DefinitionVersion = "version-2" },
		func(value *ExecutionRef) { value.InputDigest = digestOf("4") },
	}
	for i, change := range changes {
		changed := ref
		change(&changed)
		got, err := ExecutionRefDigest(changed)
		if err != nil || got == digest {
			t.Fatalf("change %d digest = %q, %v", i, got, err)
		}
	}

	left, right := ref, ref
	left.ExecutionID, left.PackGenerationID = "ab", "c"
	right.ExecutionID, right.PackGenerationID = "a", "bc"
	leftDigest, leftErr := ExecutionRefDigest(left)
	rightDigest, rightErr := ExecutionRefDigest(right)
	if leftErr != nil || rightErr != nil || leftDigest == rightDigest {
		t.Fatalf("length-framed digests = %q/%q, %v/%v", leftDigest, rightDigest, leftErr, rightErr)
	}
}

func TestExecutionRefValidationRejectsNoncanonicalFacts(t *testing.T) {
	base := goldenExecutionRef()
	for name, mutate := range map[string]func(*ExecutionRef){
		"empty execution":        func(value *ExecutionRef) { value.ExecutionID = "" },
		"trimmed generation":     func(value *ExecutionRef) { value.PackGenerationID = " generation " },
		"control definition":     func(value *ExecutionRef) { value.DefinitionRef = "flow\nname" },
		"oversize version":       func(value *ExecutionRef) { value.DefinitionVersion = strings.Repeat("v", MaxIdentifierBytes+1) },
		"bad epoch digest":       func(value *ExecutionRef) { value.ServingEpochDigest = "sha256:bad" },
		"uppercase input digest": func(value *ExecutionRef) { value.InputDigest = "sha256:" + strings.Repeat("A", 64) },
	} {
		t.Run(name, func(t *testing.T) {
			value := base
			mutate(&value)
			if ValidateExecutionRef(value) == nil {
				t.Fatal("invalid execution ref was accepted")
			}
			if digest, err := ExecutionRefDigest(value); err == nil || digest != "" {
				t.Fatalf("invalid ref digest = %q, %v", digest, err)
			}
		})
	}
}

func TestGetResultRequestRequiresExactBindingAndLiveDeadline(t *testing.T) {
	digest, err := ExecutionRefDigest(goldenExecutionRef())
	if err != nil {
		t.Fatal(err)
	}
	request := GetResultRequest{CommandID: "command-1", ExecutionRefDigest: digest}
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	defer cancel()
	if err := ValidateGetResultRequest(ctx, request); err != nil {
		t.Fatalf("ValidateGetResultRequest() error = %v", err)
	}

	if ValidateGetResultRequest(nil, request) == nil ||
		ValidateGetResultRequest(context.Background(), request) == nil {
		t.Fatal("request without a live deadline was accepted")
	}
	canceled, cancelNow := context.WithCancel(ctx)
	cancelNow()
	var failure *Failure
	if err := ValidateGetResultRequest(canceled, request); !errors.As(err, &failure) ||
		failure.Category != common.FailureCanceled {
		t.Fatalf("canceled request error = %#v", err)
	}
	expired, cancelExpired := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer cancelExpired()
	failure = nil
	if err := ValidateGetResultRequest(expired, request); !errors.As(err, &failure) ||
		failure.Category != common.FailureDeadlineExceeded {
		t.Fatalf("expired request error = %#v", err)
	}
	request.CommandID = " command-1"
	if ValidateGetResultRequest(ctx, request) == nil {
		t.Fatal("noncanonical command ID was accepted")
	}
	request.CommandID = "command-1"
	request.ExecutionRefDigest = "SHA256:" + strings.Repeat("0", 64)
	if ValidateGetResultRequest(ctx, request) == nil {
		t.Fatal("noncanonical ref digest was accepted")
	}
}

func TestResultDigestUsesExactBoundedPayload(t *testing.T) {
	payload := []byte(`{"status":"applied"}`)
	digest, err := ComputeResultDigest(payload)
	if err != nil {
		t.Fatalf("ComputeResultDigest() error = %v", err)
	}
	const want = "sha256:cd6933e74c95b5f528ad0e688ae02b01fd593b87c446275f50f90ee7fb2bf5ba"
	if digest != want {
		t.Fatalf("result digest = %q, want %q", digest, want)
	}
	changed, err := ComputeResultDigest([]byte(`{"status":"applied"} `))
	if err != nil || changed == digest {
		t.Fatalf("changed result digest = %q, %v", changed, err)
	}
	if value, err := ComputeResultDigest(nil); err == nil || value != "" {
		t.Fatalf("empty result digest = %q, %v", value, err)
	}
	if value, err := ComputeResultDigest(make([]byte, MaxResultPayloadBytes+1)); err == nil || value != "" {
		t.Fatalf("oversize result digest = %q, %v", value, err)
	}
}

func TestTerminalOutcomeShapesAndCommitReceipt(t *testing.T) {
	resultDigest, err := ComputeResultDigest([]byte(`{"status":"applied"}`))
	if err != nil {
		t.Fatal(err)
	}
	refDigest, err := ExecutionRefDigest(goldenExecutionRef())
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name    string
		outcome Outcome
		failure FailureCode
	}{
		{name: "applied", outcome: OutcomeApplied},
		{name: "noop", outcome: OutcomeNoop},
		{name: "rejected", outcome: OutcomeRejected, failure: FailureApplicationRejected},
		{name: "failed", outcome: OutcomeFailed, failure: FailureRuntimeExecutionFailed},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			result := ApplicationResult{
				CommandID: "command-1", ExecutionRefDigest: refDigest,
				Outcome: test.outcome, ResultDigest: resultDigest, FailureCode: test.failure,
			}
			result.RuntimeCommitReceipt, err = ComputeRuntimeCommitReceipt(result)
			if err != nil {
				t.Fatalf("ComputeRuntimeCommitReceipt() error = %v", err)
			}
			if test.outcome == OutcomeApplied {
				const want = "sha256:104e3cedc2e89ffa26dc3673ea47719abadb167d6fd21bae0688c4be857d6e38"
				if result.RuntimeCommitReceipt != want {
					t.Fatalf("Runtime commit receipt = %q, want %q", result.RuntimeCommitReceipt, want)
				}
			}
			if err := ValidateApplicationResult(result); err != nil {
				t.Fatalf("ValidateApplicationResult() error = %v", err)
			}
		})
	}
}

func TestTerminalResultRejectsMixedOrChangedFacts(t *testing.T) {
	result := validResult(t, OutcomeRejected, FailureApplicationRejected)
	for name, mutate := range map[string]func(*ApplicationResult){
		"wrong failure code":    func(value *ApplicationResult) { value.FailureCode = FailureRuntimeExecutionFailed },
		"missing result digest": func(value *ApplicationResult) { value.ResultDigest = "" },
		"foreign command":       func(value *ApplicationResult) { value.CommandID = "command-2" },
		"foreign execution":     func(value *ApplicationResult) { value.ExecutionRefDigest = digestOf("foreign") },
		"changed receipt":       func(value *ApplicationResult) { value.RuntimeCommitReceipt = digestOf("receipt") },
	} {
		t.Run(name, func(t *testing.T) {
			changed := result
			mutate(&changed)
			if ValidateApplicationResult(changed) == nil || SameApplicationResult(result, changed) {
				t.Fatal("changed terminal result was accepted as the original")
			}
		})
	}
}

func TestGetResultPendingAndTerminalAreDisjoint(t *testing.T) {
	result := validResult(t, OutcomeApplied, FailureNone)
	request := GetResultRequest{CommandID: result.CommandID, ExecutionRefDigest: result.ExecutionRefDigest}
	pending := GetResultResponse{
		CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest, State: ResultPending,
	}
	if err := ValidateGetResultResponse(request, pending); err != nil {
		t.Fatalf("pending response error = %v", err)
	}
	pending.Result = &result
	if ValidateGetResultResponse(request, pending) == nil {
		t.Fatal("pending response carried a caller-inferred result")
	}
	terminal := GetResultResponse{
		CommandID: request.CommandID, ExecutionRefDigest: request.ExecutionRefDigest,
		State: ResultTerminal, Result: &result,
	}
	if err := ValidateGetResultResponse(request, terminal); err != nil {
		t.Fatalf("terminal response error = %v", err)
	}
	terminal.ExecutionRefDigest = digestOf("other")
	if ValidateGetResultResponse(request, terminal) == nil {
		t.Fatal("response for another execution ref was accepted")
	}
}

func TestDuplicateCommandReturnsByteIdenticalApplicationResult(t *testing.T) {
	result := validResult(t, OutcomeNoop, FailureNone)
	if !SameApplicationResult(result, result) {
		t.Fatal("byte-identical duplicate result was not stable")
	}
	changed := result
	changed.Outcome = OutcomeApplied
	changed.RuntimeCommitReceipt, _ = ComputeRuntimeCommitReceipt(changed)
	if SameApplicationResult(result, changed) {
		t.Fatal("duplicate command returned a different terminal outcome")
	}
}

func goldenExecutionRef() ExecutionRef {
	return ExecutionRef{
		ExecutionID: "execution-1", PackGenerationID: "pack-generation-1",
		ServingEpochDigest: "sha256:" + strings.Repeat("1", 64),
		DefinitionRef:      "flow.connector.apply", DefinitionVersion: "version-1",
		InputDigest: "sha256:" + strings.Repeat("2", 64),
	}
}

func validResult(t *testing.T, outcome Outcome, failure FailureCode) ApplicationResult {
	t.Helper()
	refDigest, err := ExecutionRefDigest(goldenExecutionRef())
	if err != nil {
		t.Fatal(err)
	}
	resultDigest, err := ComputeResultDigest([]byte(`{"terminal":true}`))
	if err != nil {
		t.Fatal(err)
	}
	result := ApplicationResult{
		CommandID: "command-1", ExecutionRefDigest: refDigest,
		Outcome: outcome, ResultDigest: resultDigest, FailureCode: failure,
	}
	result.RuntimeCommitReceipt, err = ComputeRuntimeCommitReceipt(result)
	if err != nil {
		t.Fatal(err)
	}
	return result
}

func digestOf(value string) string { return sha256Digest([]byte(value)) }
