package relatedcountsv1

import (
	"context"
	"errors"
	"math"
	"runtime"
	"strings"
	"testing"
	"time"
)

func TestResponseCorrelationAttacksFailClosed(t *testing.T) {
	request := Request{Counts: []CountSpec{
		{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId", MatchValue: "same-parent"},
		{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId", MatchValue: "same-parent"},
		{ParentRecordID: "P-2", ChildObjectID: "Comment", RelationshipField: "OwnerId", MatchValue: "other-parent"},
	}}
	valid := Response{
		Results: []Result{
			{Index: 0, Outcome: CountOutcomeCounted},
			{Index: 1, Outcome: CountOutcomeCounted, Count: 17},
			{Index: 2, Outcome: CountOutcomeNotAvailable},
		},
		Receipts: validReceipts(),
	}
	if err := ValidateResponse(request, valid); err != nil {
		t.Fatalf("valid response = %v", err)
	}

	attacks := map[string]func([]Result) []Result{
		"swap": func(results []Result) []Result {
			results[0], results[2] = results[2], results[0]
			return results
		},
		"duplicate and omit": func(results []Result) []Result {
			results[2] = results[1]
			return results
		},
		"drop": func(results []Result) []Result {
			return results[:len(results)-1]
		},
		"append": func(results []Result) []Result {
			return append(results, Result{Index: 3, Outcome: CountOutcomeCounted, Count: 1})
		},
		"wrong first index": func(results []Result) []Result {
			results[0].Index = 1
			return results
		},
	}
	for name, attack := range attacks {
		t.Run(name, func(t *testing.T) {
			candidate := valid
			candidate.Results = attack(append([]Result(nil), valid.Results...))
			if err := ValidateResponse(request, candidate); err == nil {
				t.Fatal("correlation attack accepted")
			}
		})
	}
}

func TestEveryNonIdentityResultPermutationIsRejected(t *testing.T) {
	request := Request{Counts: []CountSpec{
		{ParentRecordID: "P-1", ChildObjectID: "First", RelationshipField: "ParentId"},
		{ParentRecordID: "P-1", ChildObjectID: "Second", RelationshipField: "ParentId"},
		{ParentRecordID: "P-1", ChildObjectID: "Third", RelationshipField: "ParentId"},
	}}
	results := []Result{{Index: 0, Outcome: CountOutcomeCounted, Count: 3}, {Index: 1, Outcome: CountOutcomeCounted, Count: 5}, {Index: 2, Outcome: CountOutcomeNotAvailable}}
	permutations := [][]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
	for _, permutation := range permutations {
		candidate := Response{Receipts: validReceipts()}
		for _, index := range permutation {
			candidate.Results = append(candidate.Results, results[index])
		}
		err := ValidateResponse(request, candidate)
		identity := permutation[0] == 0 && permutation[1] == 1 && permutation[2] == 2
		if identity && err != nil {
			t.Fatalf("identity permutation rejected: %v", err)
		}
		if !identity && err == nil {
			t.Fatalf("permutation %v accepted", permutation)
		}
	}
}

func TestExecutionFactsAndConformanceVectorCopy(t *testing.T) {
	if MaxCounts != 50 {
		t.Fatalf("MaxCounts = %d", MaxCounts)
	}
	if !FailFastAdmissionRequired {
		t.Fatal("related-counts v1 must require fail-fast admission")
	}
	first := ConformanceVectorsJSON()
	second := ConformanceVectorsJSON()
	if len(first) == 0 || len(second) == 0 {
		t.Fatal("empty conformance vectors")
	}
	first[0] ^= 0xff
	if first[0] == second[0] {
		t.Fatal("conformance vectors share mutable storage")
	}
}

func TestRequestEnvelopeAndContextAttackMatrix(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	base := Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId", MatchValue: "parent"}}}

	tooLongObject := strings.Repeat("O", MaxObjectIdentifierBytes+1)
	tooLongField := strings.Repeat("F", MaxFieldIdentifierBytes+1)
	invalidUTF8 := string([]byte{0xff})
	tests := []struct {
		name      string
		ctx       context.Context
		request   Request
		category  FailureCategory
		limit     LimitKind
		component InvalidComponent
	}{
		{name: "object one over", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: tooLongObject, RelationshipField: "ParentId", MatchValue: "parent"}}}, category: FailureResourceExhausted, limit: LimitObjectID},
		{name: "field one over", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: tooLongField, MatchValue: "parent"}}}, category: FailureResourceExhausted, limit: LimitFieldID},
		{name: "invalid value utf8", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId", MatchValue: invalidUTF8}}}, category: FailureInvalidRequest, component: InvalidComponentFieldValue},
	}

	canceled, cancelNow := context.WithCancel(ctx)
	cancelNow()
	tests = append(tests, struct {
		name      string
		ctx       context.Context
		request   Request
		category  FailureCategory
		limit     LimitKind
		component InvalidComponent
	}{name: "canceled", ctx: canceled, request: base, category: FailureCanceled})

	expired, expire := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer expire()
	tests = append(tests, struct {
		name      string
		ctx       context.Context
		request   Request
		category  FailureCategory
		limit     LimitKind
		component InvalidComponent
	}{name: "deadline exceeded", ctx: expired, request: base, category: FailureDeadlineExceeded})

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var failure *Failure
			err := ValidateRequest(tc.ctx, tc.request)
			if !errors.As(err, &failure) || failure.Category != tc.category ||
				(tc.limit != "" && failure.LimitKind != tc.limit) ||
				(tc.component != "" && failure.InvalidComponent != tc.component) {
				t.Fatalf("failure = %#v", err)
			}
		})
	}

	exact := base
	baseSize, err := SemanticRequestSize(exact)
	if err != nil {
		t.Fatal(err)
	}
	exact.Counts[0].MatchValue = strings.Repeat("x", len(base.Counts[0].MatchValue)+MaxSemanticRequestBytes-baseSize)
	if size, err := SemanticRequestSize(exact); err != nil || size != MaxSemanticRequestBytes {
		t.Fatalf("exact semantic request size = %d, %v", size, err)
	}
	if err := ValidateRequest(ctx, exact); err != nil {
		t.Fatalf("exact semantic request = %v", err)
	}
	exact.Counts[0].MatchValue += "x"
	var failure *Failure
	if err := ValidateRequest(ctx, exact); !errors.As(err, &failure) || failure.LimitKind != LimitSemanticRequest {
		t.Fatalf("one-over semantic request = %#v", err)
	}
	escapedExpansion := base
	escapedExpansion.Counts = append([]CountSpec(nil), base.Counts...)
	escapedExpansion.Counts[0].MatchValue = strings.Repeat("<", MaxSemanticRequestBytes/2)
	if err := ValidateRequest(ctx, escapedExpansion); !errors.As(err, &failure) || failure.LimitKind != LimitSemanticRequest {
		t.Fatalf("escaped semantic request = %#v", err)
	}
}

func TestOversizedMatchValueRejectedBeforeProportionalAllocation(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()
	request := Request{Counts: []CountSpec{{
		ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId",
		MatchValue: strings.Repeat("x", MaxSemanticRequestBytes*128),
	}}}
	var failure *Failure
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.LimitKind != LimitSemanticRequest {
		t.Fatalf("oversized request = %#v", err)
	}

	runtime.GC()
	var before, after runtime.MemStats
	runtime.ReadMemStats(&before)
	_ = ValidateRequest(ctx, request)
	runtime.ReadMemStats(&after)
	allocated := after.TotalAlloc - before.TotalAlloc
	t.Logf("oversized validation allocated %d bytes", allocated)
	if allocated > uint64(MaxSemanticRequestBytes*16) {
		t.Fatalf("oversized validation allocated %d bytes; input-size-proportional allocation occurred", allocated)
	}
}

func TestResponseOutcomeAttackMatrix(t *testing.T) {
	request := Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId"}}}
	for _, count := range []uint64{0, 1, math.MaxUint64} {
		response := Response{Results: []Result{{Index: 0, Outcome: CountOutcomeCounted, Count: count}}, Receipts: validReceipts()}
		if err := ValidateResponse(request, response); err != nil {
			t.Fatalf("valid count %d = %v", count, err)
		}
	}
	for _, outcome := range []CountOutcome{"", "MISSING", "FORBIDDEN", "INVALID_REQUEST", "UNKNOWN"} {
		response := Response{Results: []Result{{Index: 0, Outcome: outcome}}, Receipts: validReceipts()}
		if err := ValidateResponse(request, response); err == nil {
			t.Fatalf("unsafe item outcome %q accepted", outcome)
		}
	}
	response := Response{Results: []Result{{Index: 0, Outcome: CountOutcomeNotAvailable, Count: 1}}, Receipts: validReceipts()}
	if err := ValidateResponse(request, response); err == nil {
		t.Fatal("unavailable item carried a count")
	}
}

func FuzzValidateRequestNeverPanics(f *testing.F) {
	f.Add("P-1", "WorkItem", "ParentId", "parent")
	f.Add("", "bad object", "ParentId", "")
	f.Add("P-1", strings.Repeat("O", MaxObjectIdentifierBytes+1), string([]byte{0xff}), "value")
	f.Fuzz(func(t *testing.T, parentRecordID, objectID, fieldID, matchValue string) {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		_ = ValidateRequest(ctx, Request{Counts: []CountSpec{{ParentRecordID: parentRecordID, ChildObjectID: objectID, RelationshipField: fieldID, MatchValue: matchValue}}})
	})
}

func FuzzValidateResponseNeverPanics(f *testing.F) {
	f.Add(uint32(0), uint64(0), string(CountOutcomeCounted))
	f.Add(uint32(1), uint64(1), string(CountOutcomeNotAvailable))
	request := Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId"}}}
	f.Fuzz(func(t *testing.T, index uint32, count uint64, outcome string) {
		_ = ValidateResponse(request, Response{
			Results:  []Result{{Index: index, Count: count, Outcome: CountOutcome(outcome)}},
			Receipts: validReceipts(),
		})
	})
}

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