package sfcrmappservice

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
)

func testContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func validReceipts() semantic.Receipts {
	return semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName, CatalogRevision: 7}
}

func TestRuntimeRecordAccessClientMakesOneCoarseCallAndProjectsCRMShape(t *testing.T) {
	var calls int
	client, err := NewRuntimeRecordAccessClient(func(_ context.Context, bearer string, request semantic.Request) (semantic.Response, error) {
		calls++
		if bearer != "user-secret" || request.ObjectID == nil || *request.ObjectID != "issue" ||
			!reflect.DeepEqual(request.RecordIDs, []string{"ISS-2", "ISS-1", "ISS-2"}) {
			t.Fatalf("bearer=%q request=%#v", bearer, request)
		}
		return semantic.Response{Results: []semantic.Result{
			{RecordID: "ISS-1", Access: semantic.Access{Read: true, Update: true, Delete: true}},
			{RecordID: "ISS-2", Access: semantic.Access{Read: true, Locked: true}},
		}, Receipts: validReceipts()}, nil
	})
	if err != nil {
		t.Fatal(err)
	}
	result, receipts, err := client.Evaluate(testContext(t), "user-secret", " Issue ", []string{"ISS-2", "ISS-1", "ISS-2"})
	if err != nil {
		t.Fatal(err)
	}
	if calls != 1 || receipts.CatalogRevision != 7 || len(result) != 2 || !result["ISS-1"].HasEditAccess ||
		!result["ISS-2"].IsLocked || result["ISS-2"].HasEditAccess {
		t.Fatalf("calls=%d receipts=%#v result=%#v", calls, receipts, result)
	}
}

func TestRuntimeRecordAccessClientPreservesZeroCallRulesAndUntypedBatch(t *testing.T) {
	var requests []semantic.Request
	client, err := NewRuntimeRecordAccessClient(func(_ context.Context, _ string, request semantic.Request) (semantic.Response, error) {
		requests = append(requests, request)
		return semantic.Response{Results: []semantic.Result{}, Receipts: validReceipts()}, nil
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx := testContext(t)
	if result, _, err := client.Evaluate(ctx, "user", "", nil); err != nil || len(result) != 0 {
		t.Fatalf("empty result=%#v err=%v", result, err)
	}
	if result, _, err := client.Evaluate(ctx, "user", "", []string{" ", "", "\t"}); err != nil || len(result) != 0 {
		t.Fatalf("normalized empty result=%#v err=%v", result, err)
	}
	if _, _, err := client.Evaluate(ctx, "user", "Issue", make([]string, semantic.MaxRecords+1)); err == nil {
		t.Fatal("one-over request succeeded")
	}
	if _, _, err := client.Evaluate(ctx, "user", "bad-name", []string{"ISS-1"}); err == nil {
		t.Fatal("invalid object request succeeded")
	}
	if _, _, err := client.Evaluate(ctx, "user", "Issue", []string{strings.Repeat("x", semantic.MaxRecordIdentifierBytes+1)}); err == nil {
		t.Fatal("oversized record identifier succeeded")
	}
	if len(requests) != 0 {
		t.Fatalf("local requests reached Runtime: %#v", requests)
	}
	if _, _, err := client.Evaluate(ctx, "user", "", []string{" ISS-2 ", "ISS-1", "ISS-2", ""}); err != nil {
		t.Fatal(err)
	}
	if len(requests) != 1 || requests[0].ObjectID != nil || !reflect.DeepEqual(requests[0].RecordIDs, []string{"ISS-2", "ISS-1"}) {
		t.Fatalf("untyped Runtime requests = %#v", requests)
	}
}

func TestRuntimeRecordAccessClientRejectsUncorrelatedDisclosure(t *testing.T) {
	client, err := NewRuntimeRecordAccessClient(func(context.Context, string, semantic.Request) (semantic.Response, error) {
		return semantic.Response{Results: []semantic.Result{{RecordID: "ISS-NOT-REQUESTED", Access: semantic.Access{Read: true}}}, Receipts: validReceipts()}, nil
	})
	if err != nil {
		t.Fatal(err)
	}
	_, _, err = client.Evaluate(testContext(t), "user", "Issue", []string{"ISS-1"})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("uncorrelated response error = %#v", err)
	}
}
