package recyclebinv1

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

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

type recycleVector 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 TestRequestRequiresDeadlineAndClosedScope(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, scope := range []Scope{ScopeMine, ScopeAll} {
		if err := ValidateRequest(ctx, Request{Scope: scope}); err != nil {
			t.Fatalf("scope %s = %v", scope, err)
		}
	}
	for name, tc := range map[string]struct {
		ctx       context.Context
		request   Request
		category  FailureCategory
		component InvalidComponent
	}{
		"deadline": {ctx: context.Background(), request: Request{Scope: ScopeMine}, category: FailureInvalidRequest, component: InvalidComponentDeadline},
		"scope":    {ctx: ctx, request: Request{Scope: "mine"}, category: FailureInvalidRequest, component: InvalidComponentOperation},
	} {
		t.Run(name, func(t *testing.T) {
			var failure *Failure
			err := ValidateRequest(tc.ctx, tc.request)
			if !errors.As(err, &failure) || failure.Category != tc.category || failure.InvalidComponent != tc.component {
				t.Fatalf("failure = %#v", err)
			}
		})
	}
}

func TestResponseIsBoundedCanonicalAndSingleIdentity(t *testing.T) {
	response := Response{
		Items: []Item{
			{RecordID: "row-2", ObjectID: "WorkItem", DeletedDate: "2026-07-22T04:00:00Z"},
			{RecordID: "row-1", ObjectID: "WorkItem", DeletedDate: "2026-07-22T03:00:00Z"},
		},
		FailedObjects: []ObjectFailure{{ObjectID: "Alpha", Code: ObjectFailureInternal}, {ObjectID: "Zulu", Code: ObjectFailureUnknown}},
		Receipts:      Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
	if err := ValidateResponse(response); err != nil {
		t.Fatalf("valid response = %v", err)
	}
	encoded, err := json.Marshal(response)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Count(encoded, []byte(`"record_id"`)) != 2 || bytes.Contains(encoded, []byte(`"id"`)) {
		t.Fatalf("record identity was duplicated in neutral response: %s", encoded)
	}

	for name, mutate := range map[string]func(*Response){
		"duplicate record":  func(r *Response) { r.Items[1].RecordID = r.Items[0].RecordID },
		"wrong item order":  func(r *Response) { r.Items[1].DeletedDate = "2026-07-22T05:00:00Z" },
		"duplicate failure": func(r *Response) { r.FailedObjects[1].ObjectID = r.FailedObjects[0].ObjectID },
		"unknown failure":   func(r *Response) { r.FailedObjects[0].Code = "SQL_ERROR" },
		"wrong receipt":     func(r *Response) { r.Receipts.CapabilityVersion = "runtime.records.other.v1" },
	} {
		t.Run(name, func(t *testing.T) {
			copy := response
			copy.Items = append([]Item(nil), response.Items...)
			copy.FailedObjects = append([]ObjectFailure(nil), response.FailedObjects...)
			mutate(&copy)
			if err := ValidateResponse(copy); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
}

func TestResponseOrdersParsedInstantsAndTreatsEqualInstantsAsSemanticTies(t *testing.T) {
	base := Response{
		Items: []Item{
			{RecordID: "row-a", ObjectID: "WorkItem", DeletedDate: "2026-07-22T04:00:00.1Z"},
			{RecordID: "row-b", ObjectID: "WorkItem", DeletedDate: "2026-07-22T04:00:00.1Z"},
			{RecordID: "row-c", ObjectID: "WorkItem", DeletedDate: "2026-07-22T04:00:00.09Z"},
		},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
	if err := ValidateResponse(base); err != nil {
		t.Fatalf("canonical response = %v", err)
	}

	equalInstantTie := base
	equalInstantTie.Items = append([]Item(nil), base.Items...)
	equalInstantTie.Items[0], equalInstantTie.Items[1] = equalInstantTie.Items[1], equalInstantTie.Items[0]
	if err := ValidateResponse(equalInstantTie); err != nil {
		t.Fatalf("equal-instant semantic tie = %v", err)
	}

	wrongInstant := base
	wrongInstant.Items = append([]Item(nil), base.Items...)
	wrongInstant.Items[1].DeletedDate = "2026-07-22T04:00:00.08Z"
	wrongInstant.Items[2].DeletedDate = "2026-07-22T04:00:00.09Z"
	if err := ValidateResponse(wrongInstant); err == nil {
		t.Fatal("expected chronological inversion to be rejected")
	}
}

func TestResponseBounds(t *testing.T) {
	response := Response{Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}}
	response.Items = make([]Item, MaxItems+1)
	for i := range response.Items {
		response.Items[i] = Item{RecordID: "row-" + strings.Repeat("x", i%4) + string(rune('A'+i%26)), ObjectID: "WorkItem", DeletedDate: "2026-07-22T04:00:00Z"}
	}
	var failure *Failure
	if err := ValidateResponse(response); !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != LimitRecordCount {
		t.Fatalf("item limit = %#v", err)
	}

	response = Response{Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}}
	response.FailedObjects = make([]ObjectFailure, MaxFailedObjects+1)
	for i := range response.FailedObjects {
		response.FailedObjects[i] = ObjectFailure{ObjectID: "Object_" + strings.Repeat("x", i/26) + string(rune('A'+i%26)), Code: ObjectFailureUnknown}
	}
	if err := ValidateResponse(response); !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != LimitRecordCount {
		t.Fatalf("failed-object limit = %#v", err)
	}

	response = Response{
		Items: []Item{{
			RecordID:    "row-large",
			RecordName:  strings.Repeat("x", MaxSemanticResponseBytes),
			ObjectID:    "WorkItem",
			DeletedDate: "2026-07-22T04:00:00Z",
		}},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
	if err := ValidateResponse(response); !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.LimitKind != LimitSemanticResponse {
		t.Fatalf("response-byte limit = %#v", err)
	}
}

func TestCanceledRequestFailsBeforeValidation(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	var failure *Failure
	if err := ValidateRequest(ctx, Request{Scope: "invalid"}); !errors.As(err, &failure) || failure.Category != FailureCanceled {
		t.Fatalf("canceled request = %#v", err)
	}
}

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.recycle-bin-list.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.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)
		}
	}
}
