package externaleffectv1

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"
	"time"
)

func TestExternalEffectLifecycleValidation(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateClaimRequest(ctx, ClaimRequest{WorkerID: "integration-1", Limit: 1}); err != nil {
		t.Fatal(err)
	}
	op := validOperation()
	response := ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}
	if err := ValidateClaimResponse(response); err != nil {
		t.Fatal(err)
	}
	if err := ValidateClaimResponseFor(ClaimRequest{WorkerID: "integration-1", Limit: 1}, response); err != nil {
		t.Fatal(err)
	}
	complete := CompleteRequest{EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1, Results: []OperationResult{{
		OperationID: op.OperationID, RequestDigest: op.RequestDigest, Outcome: OutcomeResponseReceived, StatusCode: 200,
	}}}
	if err := ValidateCompleteRequest(ctx, complete); err != nil {
		t.Fatal(err)
	}
	if err := ValidateCompleteRequestForClaim(ctx, response, complete); err != nil {
		t.Fatal(err)
	}
	if err := ValidateCompleteResponse(complete, CompleteResponse{EffectID: complete.EffectID, State: CompletionFinalized, Receipts: validReceipts()}); err != nil {
		t.Fatal(err)
	}
}

func TestExternalEffectCompletionBindsExactClaim(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	op := validOperation()
	claim := ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}
	valid := CompleteRequest{EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1, Results: []OperationResult{{
		OperationID: op.OperationID, RequestDigest: op.RequestDigest, Outcome: OutcomeResponseReceived, StatusCode: 200,
	}}}
	mutations := []struct {
		name   string
		mutate func(*CompleteRequest)
	}{
		{name: "effect", mutate: func(r *CompleteRequest) { r.EffectID = "effect-other" }},
		{name: "claim token", mutate: func(r *CompleteRequest) { r.ClaimToken = "claim-other" }},
		{name: "revision", mutate: func(r *CompleteRequest) { r.CatalogRevision++ }},
		{name: "operation", mutate: func(r *CompleteRequest) { r.Results[0].OperationID = "op-other" }},
		{name: "digest", mutate: func(r *CompleteRequest) {
			r.Results[0].RequestDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
		}},
	}
	for _, test := range mutations {
		t.Run(test.name, func(t *testing.T) {
			request := valid
			request.Results = append([]OperationResult(nil), valid.Results...)
			test.mutate(&request)
			if err := ValidateCompleteRequestForClaim(ctx, claim, request); err == nil {
				t.Fatal("accepted completion outside the exact claim")
			}
		})
	}
}

func TestExternalEffectResponseHonorsClaimLimitAndRevision(t *testing.T) {
	op := validOperation()
	response := ClaimResponse{
		Effects: []Effect{
			{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}},
			{EffectID: "effect-2", ClaimToken: "claim-2", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}},
		},
		Receipts: validReceipts(),
	}
	revision := uint64(2)
	if err := ValidateClaimResponseFor(ClaimRequest{WorkerID: "integration-1", Limit: 1}, response); err == nil {
		t.Fatal("accepted more effects than requested")
	}
	if err := ValidateClaimResponseFor(ClaimRequest{WorkerID: "integration-1", Limit: 2, MinCatalogRevision: &revision}, response); err == nil {
		t.Fatal("accepted stale catalog revision")
	}
}

func TestExternalEffectReceiptsRequireExactNonzeroAuthority(t *testing.T) {
	op := validOperation()
	base := ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}
	for _, mutate := range []func(*Receipts){
		func(r *Receipts) { r.CatalogRevision = 0 },
		func(r *Receipts) { r.SchemaGeneration = 0 },
		func(r *Receipts) { r.EngineGeneration = 0 },
		func(r *Receipts) { r.SecurityEpoch = 0 },
	} {
		response := base
		mutate(&response.Receipts)
		if err := ValidateClaimResponse(response); err == nil {
			t.Fatalf("accepted unbound receipts %#v", response.Receipts)
		}
	}
}

func TestExternalEffectSemanticJSONUsesStableSnakeCase(t *testing.T) {
	encoded, err := json.Marshal(ClaimRequest{WorkerID: "integration-1", Limit: 1})
	if err != nil {
		t.Fatal(err)
	}
	if got := string(encoded); !strings.Contains(got, `"worker_id"`) || strings.Contains(got, "WorkerID") {
		t.Fatalf("ClaimRequest JSON = %s", got)
	}
}

func TestExternalEffectDigestBindsRetryPolicy(t *testing.T) {
	op := validOperation()
	op.RetryPolicy.MaxAttempts++
	if err := ValidateClaimResponse(ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}); err == nil {
		t.Fatal("accepted retry policy outside the frozen request digest")
	}
}

func TestExternalEffectDigestCanonicalizesEmptyRepeatedFields(t *testing.T) {
	op := validOperation()
	withEmpty := op
	withEmpty.Query = []QueryParameter{}
	withEmpty.RetryPolicy.RetryableStatusCodes = []uint32{}
	withEmpty.Body = []byte{}
	op.Body = nil
	if got, want := ComputeRequestDigest(withEmpty), ComputeRequestDigest(op); got != want {
		t.Fatalf("empty repeated fields changed digest: got %q want %q", got, want)
	}
}

func TestExternalEffectRequiresCanonicalSetOrdering(t *testing.T) {
	op := validOperation()
	op.Query = []QueryParameter{{Name: "z", Value: "1"}, {Name: "a", Value: "1"}}
	op.RequestDigest = ComputeRequestDigest(op)
	if err := ValidateClaimResponse(ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}); err == nil {
		t.Fatal("accepted a non-canonical query order")
	}
	op = validOperation()
	op.Headers[0].Values = []string{"z", "a"}
	op.RequestDigest = ComputeRequestDigest(op)
	if err := ValidateClaimResponse(ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
		Receipts: validReceipts(),
	}); err == nil {
		t.Fatal("accepted a non-canonical header-value order")
	}
}

func TestExternalEffectRequiresCanonicalRetryStatusCodeOrdering(t *testing.T) {
	tests := []struct {
		name      string
		statuses  []uint32
		wantValid bool
	}{
		{name: "sorted", statuses: []uint32{404, 429, 500}, wantValid: true},
		{name: "descending", statuses: []uint32{500, 404}},
		{name: "duplicate", statuses: []uint32{500, 500}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			op := validOperation()
			op.RetryPolicy.RetryableStatusCodes = test.statuses
			op.RequestDigest = ComputeRequestDigest(op)
			err := ValidateClaimResponse(ClaimResponse{
				Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
				Receipts: validReceipts(),
			})
			if test.wantValid && err != nil {
				t.Fatalf("rejected canonical retry status codes: %v", err)
			}
			if !test.wantValid && err == nil {
				t.Fatal("accepted non-canonical retry status codes")
			}
		})
	}
}

func TestExternalEffectDigestHasStableV1Vector(t *testing.T) {
	if got, want := ComputeRequestDigest(validOperation()), "sha256:8b9df0e8605a2e43a41a69b74060ea37092aa935ec80ea715074a2dc5be37be6"; got != want {
		t.Fatalf("v1 digest = %q, want %q", got, want)
	}
}

func TestExternalEffectRejectsAuthorityBearingRelativePath(t *testing.T) {
	tests := []struct {
		name         string
		relativePath string
	}{
		{name: "network path", relativePath: "//attacker.example/path"},
		{name: "backslash authority", relativePath: "/\\attacker.example/path"},
		{name: "embedded backslash", relativePath: "/safe\\segment"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			op := validOperation()
			op.RelativePath = test.relativePath
			op.RequestDigest = ComputeRequestDigest(op)
			if err := ValidateClaimResponse(ClaimResponse{
				Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}},
				Receipts: validReceipts(),
			}); err == nil {
				t.Fatalf("accepted authority-bearing relative path %q", test.relativePath)
			}
		})
	}
}

func TestExternalEffectRejectsAggregateBeforeNestedDigestWork(t *testing.T) {
	body := []byte(strings.Repeat("x", MaxRequestBodyBytes))
	operations := make([]Operation, 13)
	for i := range operations {
		operations[i] = validOperation()
		operations[i].OperationID = "op-" + strings.Repeat("x", i+1)
		operations[i].Body = body
		// A well-shaped but deliberately stale digest proves the aggregate budget
		// is enforced before request hashing/identity validation.
		operations[i].RequestDigest = "sha256:" + strings.Repeat("a", 64)
	}
	err := ValidateClaimResponse(ClaimResponse{
		Effects:  []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: operations}},
		Receipts: validReceipts(),
	})
	var failure *Failure
	if !errors.As(err, &failure) || failure.Category != "RESOURCE_EXHAUSTED" {
		t.Fatalf("oversized aggregate = %#v", err)
	}
}

func TestExternalEffectCompletionRequiresTerminalSuffixToBeSkipped(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	op := validOperation()
	request := CompleteRequest{EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1, Results: []OperationResult{
		{OperationID: op.OperationID, RequestDigest: op.RequestDigest, Outcome: OutcomeNoResponse, FailureCode: FailureNoResponse},
		{OperationID: "op-2", RequestDigest: op.RequestDigest, Outcome: OutcomeResponseReceived, StatusCode: 200},
	}}
	if err := ValidateCompleteRequest(ctx, request); err == nil {
		t.Fatal("accepted execution after terminal outcome")
	}
	request.Results[1] = OperationResult{OperationID: "op-2", RequestDigest: op.RequestDigest, Outcome: OutcomeSkipped}
	if err := ValidateCompleteRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
}

func TestExternalEffectCompletionRejectsProviderSpecificFailureCode(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	op := validOperation()
	request := CompleteRequest{EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1, Results: []OperationResult{{
		OperationID: op.OperationID, RequestDigest: op.RequestDigest, Outcome: OutcomeNoResponse, FailureCode: "salesforce.timeout",
	}}}
	if err := ValidateCompleteRequest(ctx, request); err == nil {
		t.Fatal("accepted provider-specific failure code outside the neutral v1 vocabulary")
	}
}

func TestExternalEffectRejectsSecretHeadersAndStaleDigest(t *testing.T) {
	op := validOperation()
	op.Headers = append(op.Headers, Header{Name: "authorization", Values: []string{"secret"}})
	op.RequestDigest = ComputeRequestDigest(op)
	if err := ValidateClaimResponse(ClaimResponse{Effects: []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}}, Receipts: validReceipts()}); err == nil {
		t.Fatal("accepted secret-bearing request header")
	}
	for _, name := range []string{"x-api-key", "x-auth-token", "x-custom-secret"} {
		op = validOperation()
		op.Headers = append(op.Headers, Header{Name: name, Values: []string{"secret"}})
		op.RequestDigest = ComputeRequestDigest(op)
		if err := ValidateClaimResponse(ClaimResponse{Effects: []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}}, Receipts: validReceipts()}); err == nil {
			t.Fatalf("accepted non-allowlisted request header %q", name)
		}
	}
	op = validOperation()
	op.Body = []byte(`{"changed":true}`)
	if err := ValidateClaimResponse(ClaimResponse{Effects: []Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1, Operations: []Operation{op}}}, Receipts: validReceipts()}); err == nil {
		t.Fatal("accepted stale request digest")
	}
}

func TestExternalEffectRejectsInformationalResponse(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	op := validOperation()
	request := CompleteRequest{EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1, Results: []OperationResult{{
		OperationID: op.OperationID, RequestDigest: op.RequestDigest, Outcome: OutcomeResponseReceived, StatusCode: 103,
	}}}
	if err := ValidateCompleteRequest(ctx, request); err == nil {
		t.Fatal("accepted informational response as a final provider outcome")
	}
}

func validOperation() Operation {
	op := Operation{OperationID: "op-1", Method: "PATCH", RelativePath: "/objects/1", Headers: []Header{{Name: "content-type", Values: []string{"application/json"}}}, Body: []byte(`{"name":"Ada"}`), CredentialRef: "credential-1", EndpointClass: "objects.write", RetryPolicy: RetryPolicy{MaxAttempts: 1, MultiplierMilli: 1000}}
	op.RequestDigest = ComputeRequestDigest(op)
	return op
}

func validReceipts() Receipts {
	return Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1}
}
