package recordaccessv1

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

func TestValidateRequestBoundariesAndCurrentEmptyIdentifierSemantics(t *testing.T) {
	if !FailFastAdmissionRequired {
		t.Fatal("record-access v1 must reject overload instead of queueing without bound")
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	object := strings.Repeat("O", MaxObjectIdentifierBytes)
	recordIDs := make([]string, MaxRecords)
	for i := range recordIDs {
		recordIDs[i] = strings.Repeat("r", MaxRecordIdentifierBytes)
	}
	if err := ValidateRequest(ctx, Request{ObjectID: &object, RecordIDs: recordIDs}); err != nil {
		t.Fatalf("ValidateRequest(maximum) = %v", err)
	}
	if err := ValidateRequest(ctx, Request{RecordIDs: []string{"", "   "}}); err != nil {
		t.Fatalf("ValidateRequest(empty/missing identifiers) = %v", err)
	}
	if err := ValidateRequest(ctx, Request{}); err != nil {
		t.Fatalf("ValidateRequest(empty batch) = %v", err)
	}

	tests := []struct {
		name    string
		request Request
		kind    LimitKind
		invalid InvalidComponent
	}{
		{name: "batch one over", request: Request{RecordIDs: make([]string, MaxRecords+1)}, kind: LimitRecordCount},
		{name: "object one over", request: Request{ObjectID: ptr(strings.Repeat("O", MaxObjectIdentifierBytes+1)), RecordIDs: []string{"R"}}, kind: LimitObjectID},
		{name: "record one over", request: Request{RecordIDs: []string{strings.Repeat("r", MaxRecordIdentifierBytes+1)}}, kind: LimitRecordID},
		{name: "invalid object grammar", request: Request{ObjectID: ptr("bad object"), RecordIDs: []string{"R"}}, invalid: InvalidComponentObjectID},
		{name: "invalid utf8 record", request: Request{RecordIDs: []string{string([]byte{0xff})}}, invalid: InvalidComponentRecordID},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var failure *Failure
			err := ValidateRequest(ctx, tc.request)
			if !errors.As(err, &failure) || (tc.kind != "" && failure.LimitKind != tc.kind) ||
				(tc.invalid != "" && failure.InvalidComponent != tc.invalid) {
				t.Fatalf("ValidateRequest() = %#v", err)
			}
		})
	}
}

func TestValidateRequestRequiresLiveDeadline(t *testing.T) {
	request := Request{RecordIDs: []string{"R-1"}}
	var failure *Failure
	if err := ValidateRequest(context.Background(), request); !errors.As(err, &failure) ||
		failure.Category != FailureInvalidRequest || failure.InvalidComponent != InvalidComponentDeadline {
		t.Fatalf("missing deadline = %#v", err)
	}

	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	if err := ValidateRequest(canceled, request); !errors.As(err, &failure) || failure.Category != FailureCanceled {
		t.Fatalf("canceled = %#v", err)
	}

	expired, expire := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer expire()
	if err := ValidateRequest(expired, request); !errors.As(err, &failure) || failure.Category != FailureDeadlineExceeded {
		t.Fatalf("expired = %#v", err)
	}

	// No capability-specific numeric maximum is currently approved. In
	// particular, selected-root's 2-second attempt ceiling must not leak into
	// record-access merely because both operations share the Runtime wire.
	long, cancelLong := context.WithTimeout(context.Background(), 24*time.Hour)
	defer cancelLong()
	if err := ValidateRequest(long, request); err != nil {
		t.Fatalf("long but finite deadline = %v", err)
	}
}

func TestValidateResponseMasksUnreadableAndRequiresCanonicalWholeResult(t *testing.T) {
	valid := Response{
		Results: []Result{
			{RecordID: "A-1", Access: Access{Read: true, Update: true}},
			{RecordID: "W-1", Access: Access{Read: true}},
		},
		Receipts: validReceipts(),
	}
	request := Request{RecordIDs: []string{"A-1", "W-1", "MISSING"}}
	if err := ValidateResponse(request, valid); err != nil {
		t.Fatalf("ValidateResponse(valid) = %v", err)
	}

	tests := []struct {
		name   string
		mutate func(*Response)
	}{
		{name: "unordered", mutate: func(r *Response) { r.Results[0], r.Results[1] = r.Results[1], r.Results[0] }},
		{name: "duplicate", mutate: func(r *Response) { r.Results[1].RecordID = r.Results[0].RecordID }},
		{name: "read denied entry leaks existence", mutate: func(r *Response) { r.Results[0].Access.Read = false }},
		{name: "unrequested readable entry leaks existence", mutate: func(r *Response) { r.Results[0].RecordID = "OTHER" }},
		{name: "locked but mutable", mutate: func(r *Response) { r.Results[0].Access.Locked = true }},
		{name: "wrong capability receipt", mutate: func(r *Response) { r.Receipts.CapabilityVersion = "runtime.records.other.v1" }},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			response := valid
			response.Results = append([]Result(nil), valid.Results...)
			tc.mutate(&response)
			if err := ValidateResponse(request, response); err == nil {
				t.Fatal("ValidateResponse() error = nil")
			}
		})
	}

	if err := ValidateResponse(Request{}, Response{
		Results:  []Result{{RecordID: "SURPRISE", Access: Access{Read: true}}},
		Receipts: validReceipts(),
	}); err == nil {
		t.Fatal("empty request accepted a result")
	}
	if err := ValidateResponse(Request{RecordIDs: []string{"   "}}, Response{
		Results:  []Result{{RecordID: "   ", Access: Access{Read: true}}},
		Receipts: validReceipts(),
	}); err == nil {
		t.Fatal("whitespace-only missing identifier produced a result")
	}
}

func TestRetryClassification(t *testing.T) {
	for _, category := range []FailureCategory{
		FailureInvalidRequest, FailureUnauthenticated, FailureForbidden, FailureMissing,
		FailureCanceled, FailureDeadlineExceeded, FailureResourceExhausted, FailureIncompatible, FailureInternal,
	} {
		if got := RetryDispositionFor(category); got != RetryNever {
			t.Fatalf("RetryDispositionFor(%s) = %s", category, got)
		}
	}
	if got := RetryDispositionFor(FailureStaleState); got != RetryAfterStateRefresh {
		t.Fatalf("stale retry = %s", got)
	}
	if got := RetryDispositionFor(FailureUnavailable); got != RetryWithBackoff {
		t.Fatalf("unavailable retry = %s", got)
	}
	if ValidTopLevelFailure(FailureMissing) {
		t.Fatal("MISSING must be an omitted batch result, not a top-level failure")
	}
	for _, category := range []FailureCategory{
		FailureInvalidRequest, FailureUnauthenticated, FailureForbidden, FailureCanceled,
		FailureDeadlineExceeded, FailureResourceExhausted, FailureIncompatible,
		FailureStaleState, FailureUnavailable, FailureInternal,
	} {
		if !ValidTopLevelFailure(category) {
			t.Fatalf("ValidTopLevelFailure(%s) = false", category)
		}
	}
}

type vectorSuite struct {
	SchemaVersion string         `json:"schema_version"`
	Vectors       []recordVector `json:"vectors"`
}

type recordVector struct {
	Name     string         `json:"name"`
	Covers   []string       `json:"covers"`
	Request  Request        `json:"request"`
	Response *Response      `json:"response,omitempty"`
	Failure  *vectorFailure `json:"failure,omitempty"`
}

type vectorFailure struct {
	Category FailureCategory  `json:"category"`
	Retry    RetryDisposition `json:"retry"`
}

func TestImmutableNeutralConformanceVectors(t *testing.T) {
	data := ConformanceVectorsJSON()
	if bytes.Contains(bytes.ToLower(data), []byte("salesforce")) ||
		bytes.Contains(bytes.ToLower(data), []byte("sobject")) ||
		bytes.Contains(bytes.ToLower(data), []byte("sfcrm")) {
		t.Fatal("product vocabulary leaked into neutral vectors")
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	var suite vectorSuite
	if err := decoder.Decode(&suite); err != nil {
		t.Fatal(err)
	}
	if suite.SchemaVersion != "nexuscrm.runtime.record-access.conformance/v1" {
		t.Fatalf("schema = %q", suite.SchemaVersion)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	seenNames := map[string]struct{}{}
	covered := map[string]bool{}
	for _, vector := range suite.Vectors {
		if vector.Name == "" {
			t.Fatal("empty vector name")
		}
		if _, duplicate := seenNames[vector.Name]; duplicate {
			t.Fatalf("duplicate vector %q", vector.Name)
		}
		seenNames[vector.Name] = struct{}{}
		for _, coverage := range vector.Covers {
			covered[coverage] = true
		}
		if err := ValidateRequest(ctx, vector.Request); err != nil {
			t.Fatalf("%s request = %v", vector.Name, err)
		}
		if (vector.Response == nil) == (vector.Failure == nil) {
			t.Fatalf("%s must declare exactly one response or failure", vector.Name)
		}
		if vector.Response != nil {
			if err := ValidateResponse(vector.Request, *vector.Response); err != nil {
				t.Fatalf("%s response = %v", vector.Name, err)
			}
		} else if !ValidTopLevelFailure(vector.Failure.Category) ||
			vector.Failure.Retry != RetryDispositionFor(vector.Failure.Category) {
			t.Fatalf("%s failure = %#v", vector.Name, vector.Failure)
		}
	}
	for _, required := range []string{
		"empty-batch", "exactly-one-rpc",
		"object-crud", "owner", "sharing", "rls-denied", "missing", "identical-omission",
		"approval-lock", "metadata-lock-rule", "generic-delete-blocker", "mixed-record-types",
		"duplicates", "stable-ordering", "cancellation", "deadline", "no-partial-result",
	} {
		if !covered[required] {
			t.Fatalf("missing vector coverage %q", required)
		}
	}

	copyData := ConformanceVectorsJSON()
	copyData[0] ^= 0xff
	if reflect.DeepEqual(copyData, ConformanceVectorsJSON()) {
		t.Fatal("ConformanceVectorsJSON returned shared mutable storage")
	}
}

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

func validReceipts() Receipts {
	return Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
}
