package recordmutationv1

import (
	"context"
	"errors"
	"testing"
	"time"
)

func TestMutationUseCaseIsExplicitAndDigestCompatible(t *testing.T) {
	request := typedRequest()
	oldDigest, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	if want := "sha256:70018a0c7f3904b071bbdf302a3e16bc348a66f1427790a15ff356789c70a0a3"; oldDigest != want {
		t.Fatalf("N-1 command digest changed: got %s, want %s", oldDigest, want)
	}

	request.UseCase = MutationUseCaseSingle
	explicitDigest, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	if explicitDigest == oldDigest {
		t.Fatal("explicit mutation use case did not bind the command digest")
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request.Operations = append(request.Operations, Operation{Kind: OperationDelete, RecordID: "other"})
	var failure *Failure
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.InvalidComponent != InvalidComponentOperation {
		t.Fatalf("multi-operation single-record use case = %#v", err)
	}
	request.UseCase = MutationUseCaseCollection
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("collection use case = %v", err)
	}
}

func TestLosslessCallerJSONValues(t *testing.T) {
	number := "12345678901234567890.00100e-2"
	rawJSON := `{"z":2,"a":[0,false,""],"nested":{"b":1,"a":null}}`
	request := Request{CommandID: "lossless-source-json", BatchMode: BatchIndependent, UseCase: MutationUseCaseSingle, Operations: []Operation{{
		Kind: OperationCreate, ObjectID: "WorkItem", Fields: []FieldValue{
			{Name: "Amount", Kind: ValueKindNumber, Number: &number},
			{Name: "Payload", Kind: ValueKindJSON, JSON: &rawJSON},
		},
	}}}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	normalized := NormalizeRequest(request)
	if got, want := *normalized.Operations[0].Fields[1].JSON, `{"a":[0,false,""],"nested":{"a":null,"b":1},"z":2}`; got != want {
		t.Fatalf("canonical JSON = %s, want %s", got, want)
	}
	if got := *normalized.Operations[0].Fields[0].Number; got != number {
		t.Fatalf("exact number = %q, want %q", got, number)
	}

	for name, value := range map[string]FieldValue{
		"leading zero number": {Name: "Value", Kind: ValueKindNumber, Number: stringPointer("01")},
		"scalar JSON":         {Name: "Value", Kind: ValueKindJSON, JSON: stringPointer(`1`)},
		"trailing JSON":       {Name: "Value", Kind: ValueKindJSON, JSON: stringPointer(`{} {}`)},
	} {
		t.Run(name, func(t *testing.T) {
			candidate := request
			candidate.Operations = []Operation{{Kind: OperationCreate, ObjectID: "WorkItem", Fields: []FieldValue{value}}}
			if err := ValidateRequest(ctx, candidate); err == nil {
				t.Fatal("invalid source value accepted")
			}
		})
	}
}

func TestCanonicalSourceJSONHasLanguageIndependentBytes(t *testing.T) {
	raw := `{ "z" : 1.0, "a":"\u003c\u0026\u003e", "line":"\u2028\u2029", "emoji":"\ud83d\ude00", "dup":1, "dup":2 }`
	got, err := CanonicalSourceJSON(raw)
	if err != nil {
		t.Fatal(err)
	}
	want := "{\"a\":\"<&>\",\"dup\":2,\"emoji\":\"😀\",\"line\":\"\u2028\u2029\",\"z\":1.0}"
	if got != want {
		t.Fatalf("canonical source JSON = %q, want %q", got, want)
	}
	if len(got) > len(raw) {
		t.Fatalf("canonical source JSON expanded from %d to %d bytes", len(raw), len(got))
	}
	second, err := CanonicalSourceJSON(got)
	if err != nil || second != got {
		t.Fatalf("canonical source JSON is not idempotent: %q, %v", second, err)
	}
}

func TestCanonicalSourceJSONRejectsAmbiguousUnicode(t *testing.T) {
	invalidUTF8 := string([]byte{'{', '"', 'x', '"', ':', '"', 0xff, '"', '}'})
	for name, raw := range map[string]string{
		"invalid UTF-8":          invalidUTF8,
		"unpaired high":          `{"x":"\ud800"}`,
		"unpaired low":           `{"x":"\udc00"}`,
		"high followed by value": `{"x":"\ud800\u0041"}`,
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := CanonicalSourceJSON(raw); err == nil {
				t.Fatal("ambiguous Unicode accepted")
			}
		})
	}
}

func FuzzCanonicalSourceJSON(f *testing.F) {
	for _, seed := range []string{
		`{}`,
		`[]`,
		`{"b":1e0,"a":[null,false,"<&>"]}`,
		`{"x":"\ud83d\ude00"}`,
	} {
		f.Add(seed)
	}
	f.Fuzz(func(t *testing.T, raw string) {
		canonical, err := CanonicalSourceJSON(raw)
		if err != nil {
			return
		}
		if len(canonical) > len(raw) {
			t.Fatalf("canonical output expanded from %d to %d bytes", len(raw), len(canonical))
		}
		second, err := CanonicalSourceJSON(canonical)
		if err != nil || second != canonical {
			t.Fatalf("canonicalization is not idempotent: %q then %q (%v)", canonical, second, err)
		}
	})
}

func TestPolicyDenialCarriesOnlyCatalogAuthoredDisplayDetail(t *testing.T) {
	valid := OperationError{
		Kind: ErrorPolicyDenied, AuthoredMessage: "This mutation surface is closed.",
		Fields: []string{},
	}
	if !validAuthoredMetadata(valid) {
		t.Fatal("valid Catalog-authored policy detail rejected")
	}
	for name, candidate := range map[string]OperationError{
		"control in message":  {Kind: ErrorPolicyDenied, AuthoredMessage: "denied\ninternal"},
		"message on internal": {Kind: ErrorInternal, AuthoredMessage: "internal"},
	} {
		if validAuthoredMetadata(candidate) {
			t.Fatalf("%s accepted", name)
		}
	}
}

func TestPolicyDenialCommandPreflightShape(t *testing.T) {
	for _, mode := range []BatchMode{BatchIndependent, BatchAtomic} {
		request := Request{CommandID: "policy-preflight", BatchMode: mode, UseCase: MutationUseCaseCollection, Operations: []Operation{
			{Kind: OperationCreate, ObjectID: "WorkItem"},
			{Kind: OperationUpdate, ObjectID: "WorkItem", RecordID: "work-1"},
			{Kind: OperationDelete, RecordID: "work-2"},
		}}
		digest, err := CommandDigest(request)
		if err != nil {
			t.Fatal(err)
		}
		response := Response{
			CommandID: request.CommandID, CommandDigest: digest, Receipts: validReceipts(),
			OutcomeExpiresAtUTC: "2026-07-20T12:30:00Z",
			Results: []OperationResult{
				{Index: 0, Kind: OperationCreate, Success: false, Errors: []OperationError{{Kind: ErrorProcessingHalted, Fields: []string{}}}},
				{Index: 1, Kind: OperationUpdate, RecordID: "work-1", Success: false, Errors: []OperationError{{Kind: ErrorPolicyDenied, AuthoredMessage: "Updates are closed.", Fields: []string{"State"}}}},
				{Index: 2, Kind: OperationDelete, RecordID: "work-2", Success: false, Errors: []OperationError{{Kind: ErrorProcessingHalted, Fields: []string{}}}},
			},
		}
		if err := ValidateResponse(request, response); err != nil {
			t.Fatalf("%s policy preflight = %v", mode, err)
		}

		invalid := response
		invalid.Results = append([]OperationResult(nil), response.Results...)
		created := true
		invalid.Results[0] = OperationResult{Index: 0, Kind: OperationCreate, RecordID: "work-created", Success: true, Created: &created, Errors: []OperationError{}}
		if err := ValidateResponse(request, invalid); err == nil {
			t.Fatalf("%s accepted a committed sibling mixed with command-preflight markers", mode)
		}
	}
}

func TestPolicyDenialCanBeATransactionRecheckOutcome(t *testing.T) {
	created := true
	for name, test := range map[string]struct {
		mode    BatchMode
		results []OperationResult
	}{
		"independent preserves committed siblings": {
			mode: BatchIndependent,
			results: []OperationResult{
				{Index: 0, Kind: OperationCreate, RecordID: "work-created", Success: true, Created: &created, Errors: []OperationError{}},
				{Index: 1, Kind: OperationUpdate, RecordID: "work-1", Success: false, Errors: []OperationError{{Kind: ErrorPolicyDenied, AuthoredMessage: "Updates are closed.", Fields: []string{"State"}}}},
				{Index: 2, Kind: OperationDelete, RecordID: "work-2", Success: true, Errors: []OperationError{}},
			},
		},
		"atomic rolls back siblings": {
			mode: BatchAtomic,
			results: []OperationResult{
				{Index: 0, Kind: OperationCreate, Success: false, Errors: []OperationError{{Kind: ErrorAtomicRolledBack, Fields: []string{}}}},
				{Index: 1, Kind: OperationUpdate, RecordID: "work-1", Success: false, Errors: []OperationError{{Kind: ErrorPolicyDenied, AuthoredMessage: "Updates are closed.", Fields: []string{"State"}}}},
				{Index: 2, Kind: OperationDelete, RecordID: "work-2", Success: false, Errors: []OperationError{{Kind: ErrorAtomicRolledBack, Fields: []string{}}}},
			},
		},
	} {
		t.Run(name, func(t *testing.T) {
			request := Request{CommandID: "policy-recheck", BatchMode: test.mode, UseCase: MutationUseCaseCollection, Operations: []Operation{
				{Kind: OperationCreate, ObjectID: "WorkItem"},
				{Kind: OperationUpdate, ObjectID: "WorkItem", RecordID: "work-1"},
				{Kind: OperationDelete, RecordID: "work-2"},
			}}
			digest, err := CommandDigest(request)
			if err != nil {
				t.Fatal(err)
			}
			response := Response{
				CommandID: request.CommandID, CommandDigest: digest, Results: test.results,
				Receipts: validReceipts(), OutcomeExpiresAtUTC: "2026-07-20T12:30:00Z",
			}
			if err := ValidateResponse(request, response); err != nil {
				t.Fatalf("transaction policy recheck outcome = %v", err)
			}
		})
	}
}

func TestRequiredFieldOutcomeCarriesCanonicalFieldsOnly(t *testing.T) {
	request := Request{CommandID: "required-fields", BatchMode: BatchIndependent, UseCase: MutationUseCaseSingle, Operations: []Operation{{
		Kind: OperationCreate, ObjectID: "WorkItem",
	}}}
	digest, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	response := Response{
		CommandID: request.CommandID, CommandDigest: digest, Receipts: validReceipts(),
		OutcomeExpiresAtUTC: "2026-07-20T12:30:00Z",
		Results: []OperationResult{{Index: 0, Kind: OperationCreate, Success: false, Errors: []OperationError{{
			Kind: ErrorRequiredFieldMissing, Fields: []string{"Name", "State"},
		}}}},
	}
	if err := ValidateResponse(request, response); err != nil {
		t.Fatal(err)
	}
	response.Results[0].Errors[0].AuthoredMessage = "Name is required"
	if err := ValidateResponse(request, response); err == nil {
		t.Fatal("required-field result accepted product error copy")
	}
}

func stringPointer(value string) *string { return &value }
