package relatedcountsv1

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

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

type countVector struct {
	Name     string    `json:"name"`
	Covers   []string  `json:"covers"`
	Request  Request   `json:"request"`
	Response *Response `json:"response"`
	Failure  *struct {
		Category FailureCategory  `json:"category"`
		Retry    RetryDisposition `json:"retry"`
	} `json:"failure"`
}

func TestRequestBoundariesAndDeadline(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	counts := make([]CountSpec, MaxCounts)
	for i := range counts {
		counts[i] = CountSpec{ParentRecordID: "parent-id", ChildObjectID: strings.Repeat("O", MaxObjectIdentifierBytes), RelationshipField: strings.Repeat("F", MaxFieldIdentifierBytes), MatchValue: "parent-key"}
	}
	if err := ValidateRequest(ctx, Request{}); err != nil {
		t.Fatalf("empty batch = %v, want valid no-op", err)
	}
	if err := ValidateRequest(ctx, Request{Counts: counts}); err != nil {
		t.Fatalf("maximum request = %v", err)
	}

	tests := []struct {
		name      string
		ctx       context.Context
		request   Request
		category  FailureCategory
		limit     LimitKind
		component InvalidComponent
	}{
		{name: "deadline required", ctx: context.Background(), request: Request{Counts: counts[:1]}, category: FailureInvalidRequest, component: InvalidComponentDeadline},
		{name: "one over", ctx: ctx, request: Request{Counts: append(counts, counts[0])}, category: FailureResourceExhausted, limit: LimitOperationCount},
		{name: "missing parent identity", ctx: ctx, request: Request{Counts: []CountSpec{{ChildObjectID: "Child", RelationshipField: "ParentId"}}}, category: FailureInvalidRequest, component: InvalidComponentRecordID},
		{name: "parent identifier one over", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: strings.Repeat("P", MaxRecordIdentifierBytes+1), ChildObjectID: "Child", RelationshipField: "ParentId"}}}, category: FailureResourceExhausted, limit: LimitRecordID},
		{name: "object grammar", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "bad object", RelationshipField: "ParentId"}}}, category: FailureInvalidRequest, component: InvalidComponentObjectID},
		{name: "field grammar", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "Child", RelationshipField: "bad field"}}}, category: FailureInvalidRequest, component: InvalidComponentRelationshipField},
		{name: "invalid value utf8", ctx: ctx, request: Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "Child", RelationshipField: "ParentId", MatchValue: string([]byte{0xff})}}}, category: FailureInvalidRequest, component: InvalidComponentFieldValue},
	}
	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)
			}
		})
	}
}

func TestResponsePreservesZeroAndOpaquePerItemOutcome(t *testing.T) {
	request := Request{Counts: []CountSpec{{ParentRecordID: "P-1", ChildObjectID: "Child", RelationshipField: "ParentId"}, {ParentRecordID: "P-2", ChildObjectID: "Other", RelationshipField: "ParentId"}}}
	response := Response{Results: []Result{{Index: 0, Outcome: CountOutcomeCounted}, {Index: 1, Outcome: CountOutcomeNotAvailable}}, Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}}
	if err := ValidateResponse(request, response); err != nil {
		t.Fatalf("valid response = %v", err)
	}

	for name, mutate := range map[string]func(*Response){
		"wrong length":           func(r *Response) { r.Results = r.Results[:1] },
		"wrong correlation":      func(r *Response) { r.Results[1].Index = 0 },
		"count with unavailable": func(r *Response) { r.Results[1].Count = 1 },
		"missing outcome":        func(r *Response) { r.Results[1].Outcome = "" },
		"wrong receipt":          func(r *Response) { r.Receipts.CapabilityVersion = "runtime.records.other.v1" },
	} {
		t.Run(name, func(t *testing.T) {
			copy := response
			copy.Results = append([]Result(nil), response.Results...)
			mutate(&copy)
			if err := ValidateResponse(request, copy); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
}

func TestParentAuthorityAndItemNonDisclosureAreStructural(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	// A predicate value cannot stand in for the parent identity Runtime must
	// resolve and authorize.
	request := Request{Counts: []CountSpec{{
		ChildObjectID: "Child", RelationshipField: "ParentId", MatchValue: "P-secret",
	}}}
	var failure *Failure
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.InvalidComponent != InvalidComponentRecordID {
		t.Fatalf("match value replaced parent authority: %#v", err)
	}

	response := Response{
		Results:  []Result{{Index: 0, Outcome: CountOutcomeNotAvailable}},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
	encoded, err := json.Marshal(response)
	if err != nil {
		t.Fatal(err)
	}
	for _, leaked := range [][]byte{[]byte("MISSING"), []byte("FORBIDDEN"), []byte("INVALID_REQUEST"), []byte("INTERNAL_FAILURE")} {
		if bytes.Contains(encoded, leaked) {
			t.Fatalf("item response leaked %q: %s", leaked, encoded)
		}
	}
}

func TestItemOutcomeAndTopLevelFailureVocabularies(t *testing.T) {
	for _, outcome := range []CountOutcome{CountOutcomeCounted, CountOutcomeNotAvailable} {
		if !outcome.Valid() {
			t.Fatalf("item outcome %s rejected", outcome)
		}
	}
	if CountOutcome("MISSING").Valid() || CountOutcome("FORBIDDEN").Valid() || CountOutcome("").Valid() {
		t.Fatal("item outcome exposes a forbidden failure distinction")
	}
	for _, category := range []FailureCategory{FailureInvalidRequest, FailureUnauthenticated, FailureForbidden, FailureCanceled, FailureDeadlineExceeded, FailureResourceExhausted, FailureIncompatible, FailureStaleState, FailureUnavailable, FailureInternal} {
		if !ValidTopLevelFailure(category) {
			t.Fatalf("top-level failure %s rejected", category)
		}
	}
	if ValidTopLevelFailure(FailureCategory("MISSING")) {
		t.Fatal("missing parent identity must not be a caller-visible RPC failure")
	}
	if RetryDispositionFor(FailureStaleState) != RetryAfterStateRefresh || RetryDispositionFor(FailureUnavailable) != RetryWithBackoff || RetryDispositionFor(FailureForbidden) != RetryNever {
		t.Fatal("unexpected retry vocabulary")
	}
}

func TestNeutralConformanceVectors(t *testing.T) {
	data := ConformanceVectorsJSON()
	for _, forbidden := range [][]byte{[]byte("salesforce"), []byte("sobject"), []byte("sfcrm"), []byte("soql")} {
		if bytes.Contains(bytes.ToLower(data), forbidden) {
			t.Fatalf("product vocabulary %q leaked into neutral vectors", forbidden)
		}
	}
	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.related-counts.conformance/v1" || len(suite.Vectors) == 0 {
		t.Fatalf("invalid vector suite %#v", suite)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	seen := map[string]struct{}{}
	for _, vector := range suite.Vectors {
		if vector.Name == "" || len(vector.Covers) == 0 || (vector.Response == nil) == (vector.Failure == nil) {
			t.Fatalf("incomplete vector %#v", vector)
		}
		if _, duplicate := seen[vector.Name]; duplicate {
			t.Fatalf("duplicate vector %q", vector.Name)
		}
		seen[vector.Name] = struct{}{}
		if err := ValidateRequest(ctx, vector.Request); err != nil {
			t.Fatalf("%s request = %v", vector.Name, err)
		}
		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) || RetryDispositionFor(vector.Failure.Category) != vector.Failure.Retry {
			t.Fatalf("%s failure = %#v", vector.Name, vector.Failure)
		}
	}
}
