package searchv1

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

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	records "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
)

func TestSearchRejectsOversizedResponseBeforeTypedValidation(t *testing.T) {
	request := Request{
		ObjectIDs:      []string{"Account"},
		Term:           "acme",
		SearchFields:   []string{"Name"},
		SelectedFields: []string{"Name"},
		Limit:          1,
	}
	value := strings.Repeat("x", MaxSemanticResponseBytes)
	response := validSearchResponse()
	response.Records[0].Fields[0].String = &value
	response.Records[0].Fields[0].Kind = records.ValueKindBoolean

	err := ValidateResponse(request, response)
	var failure *Failure
	if !errors.As(err, &failure) ||
		failure.Category != common.FailureResourceExhausted ||
		failure.LimitKind != common.LimitSemanticResponse ||
		failure.Maximum != MaxSemanticResponseBytes {
		t.Fatalf("error = %#v, want semantic-response RESOURCE_EXHAUSTED", err)
	}
}

func TestSearchValidationIsRequestCorrelatedAndTyped(t *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
	defer cancel()
	request := Request{ObjectIDs: []string{"Account"}, Term: "acme", SearchFields: []string{"Name"}, SelectedFields: []string{"Name"}, Limit: 10}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() error = %#v", err)
	}
	response := validSearchResponse()
	if err := ValidateResponse(request, response); err != nil {
		t.Fatalf("ValidateResponse() error = %v", err)
	}
	bad := validSearchResponse()
	bad.Records[0].ObjectID = "Contact"
	if ValidateResponse(request, bad) == nil {
		t.Fatal("response for an unrequested object was accepted")
	}
	bad = validSearchResponse()
	nan := math.NaN()
	bad.Records[0].Score = &nan
	if ValidateResponse(request, bad) == nil {
		t.Fatal("non-finite score was accepted")
	}
	bad = validSearchResponse()
	bad.Records[0].RecordID = strings.Repeat("r", common.MaxRecordIdentifierBytes+1)
	if ValidateResponse(request, bad) == nil {
		t.Fatal("oversized record identifier was accepted")
	}
	bad = validSearchResponse()
	bad.Records[0].Fields[0].String = nil
	if ValidateResponse(request, bad) == nil {
		t.Fatal("malformed typed field was accepted")
	}
	bad = validSearchResponse()
	bad.Records[0].Fields[0].Name = "Secret"
	if ValidateResponse(request, bad) == nil {
		t.Fatal("unselected response field was accepted")
	}
	bad = validSearchResponse()
	bad.Records = append(bad.Records, bad.Records[0])
	if ValidateResponse(request, bad) == nil {
		t.Fatal("duplicate search result was accepted")
	}
	tooLong := request
	tooLong.Term = strings.Repeat("x", MaxTermBytes+1)
	if ValidateRequest(ctx, tooLong) == nil {
		t.Fatal("oversized term was accepted")
	}
	duplicate := request
	duplicate.ObjectIDs = []string{"Account", "account"}
	if ValidateRequest(ctx, duplicate) == nil {
		t.Fatal("case-duplicate object selection was accepted")
	}
	oversizedObject := request
	oversizedObject.ObjectIDs = []string{strings.Repeat("A", common.MaxObjectIdentifierBytes+1)}
	if ValidateRequest(ctx, oversizedObject) == nil {
		t.Fatal("oversized object identifier was accepted")
	}
}

func TestSearchObjectPlansPreservePerObjectShapeAndBounds(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := Request{
		Term: "acme", Limit: 20,
		ObjectPlans: []ObjectPlan{
			{ObjectID: "Account", SearchFields: []string{"Name", "Website"}, SelectedFields: []string{"Name"}, Limit: 10},
			{ObjectID: "Contact", SearchFields: []string{"Name", "Email"}, SelectedFields: []string{"Name", "Email"}, Limit: 10},
		},
	}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() error = %v", err)
	}
	plans := ObjectPlansForExecution(request)
	if len(plans) != 2 || plans[0].ObjectID != "Account" ||
		len(plans[0].SearchFields) != 2 || plans[1].SelectedFields[1] != "Email" {
		t.Fatalf("execution plans = %#v", plans)
	}
	accountName := "Acme"
	contactName := "Ada"
	contactEmail := "ada@example.test"
	response := Response{
		Records: []Record{
			{ObjectID: "Account", RecordID: "001", Fields: []records.Field{{Name: "Name", Kind: records.ValueKindString, String: &accountName}}},
			{ObjectID: "Contact", RecordID: "003", Fields: []records.Field{
				{Name: "Email", Kind: records.ValueKindString, String: &contactEmail},
				{Name: "Name", Kind: records.ValueKindString, String: &contactName},
			}},
		},
		Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
	if err := ValidateResponse(request, response); err != nil {
		t.Fatalf("ValidateResponse() error = %v", err)
	}
	wrongObjectField := response
	wrongObjectField.Records = append([]Record(nil), response.Records...)
	wrongObjectField.Records[0] = response.Records[0]
	wrongObjectField.Records[0].Fields = []records.Field{{Name: "Email", Kind: records.ValueKindString, String: &contactEmail}}
	if ValidateResponse(request, wrongObjectField) == nil {
		t.Fatal("field selected only for another object was accepted")
	}
	perObjectOverflow := request
	perObjectOverflow.ObjectPlans = append([]ObjectPlan(nil), request.ObjectPlans...)
	perObjectOverflow.ObjectPlans[0].Limit = 1
	overflowResponse := response
	overflowResponse.Records = append([]Record(nil), response.Records...)
	overflowResponse.Records = append(overflowResponse.Records, Record{
		ObjectID: "Account", RecordID: "002",
		Fields: []records.Field{{Name: "Name", Kind: records.ValueKindString, String: &accountName}},
	})
	if ValidateResponse(perObjectOverflow, overflowResponse) == nil {
		t.Fatal("response exceeded one ObjectPlan limit while remaining under the aggregate limit")
	}

	mixed := request
	mixed.ObjectIDs = []string{"Account"}
	mixed.SearchFields = []string{"Name"}
	mixed.SelectedFields = []string{"Name"}
	if ValidateRequest(ctx, mixed) == nil {
		t.Fatal("mixed legacy and object-plan shapes were accepted")
	}
	duplicate := request
	duplicate.ObjectPlans[1].ObjectID = "account"
	if ValidateRequest(ctx, duplicate) == nil {
		t.Fatal("case-duplicate object plans were accepted")
	}
	overBudget := request
	overBudget.ObjectPlans[0].Limit = MaxRecords + 1
	if ValidateRequest(ctx, overBudget) == nil {
		t.Fatal("object plan limit above MaxRecords was accepted")
	}
}

func validSearchResponse() Response {
	return Response{
		Records: []Record{{
			ObjectID: "Account", RecordID: "001",
			Fields: []records.Field{{Name: "Name", Kind: records.ValueKindString, String: ptr("Acme")}},
		}},
		Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName},
	}
}

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