package resourceaccessv1

import (
	"context"
	"errors"
	"regexp"
	"testing"
	"time"

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

func TestResourceAccessRequestShapes(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, request := range []Request{
		{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"},
		{CommandID: "replace-1", Operation: ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []GrantInput{{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}}},
		{CommandID: "transfer-1", Operation: TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2"},
	} {
		if err := ValidateRequest(ctx, request); err != nil {
			t.Fatalf("%#v: %v", request, err)
		}
	}
}

func TestResourceAccessRejectsMissingContextAsInvalidDeadline(t *testing.T) {
	var failure *common.Failure
	err := ValidateRequest(nil, Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"})
	if !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest || failure.InvalidComponent != common.InvalidComponentDeadline {
		t.Fatalf("ValidateRequest(nil) = %#v", err)
	}
}

func TestResourceAccessRejectsDuplicateGrantsAndControlIdentifiers(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	grant := GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	samePrincipalDifferentLevel := grant
	samePrincipalDifferentLevel.AccessLevel = "View"
	for _, request := range []Request{
		{CommandID: "create-1", Operation: CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grants: []GrantInput{grant, grant}},
		{CommandID: "replace-1", Operation: ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []GrantInput{grant, grant}},
		{CommandID: "replace-1", Operation: ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []GrantInput{grant, samePrincipalDifferentLevel}},
		{Operation: ListGrants, ResourceType: "folder\x00unsafe", ResourceID: "folder-1"},
		{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder\u0001unsafe"},
		{CommandID: "ensure-1", Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &GrantInput{PrincipalID: "user\x00unsafe", PrincipalType: "User", AccessLevel: "Manage"}},
		{Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant},
		{CommandID: "smuggled-read-command", Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"},
	} {
		var failure *common.Failure
		if err := ValidateRequest(ctx, request); !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest {
			t.Fatalf("ValidateRequest(%#v) = %#v", request, err)
		}
	}
}

func TestResourceAccessRejectsNonTransportableSingletonCreateShape(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	grant := GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	request := Request{
		CommandID: "create-1", Operation: CreateGrant, ResourceType: "folder", ResourceID: "folder-1",
		Grant: &grant, Grants: []GrantInput{},
	}
	var failure *common.Failure
	if err := ValidateRequest(ctx, request); !errors.As(err, &failure) ||
		failure.Category != common.FailureInvalidRequest || failure.InvalidComponent != common.InvalidComponentCommandData {
		t.Fatalf("ValidateRequest() = %#v, want invalid command data", err)
	}
}

func TestResourceAccessClassifiesCommandIdentityFailures(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	grant := GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	for _, request := range []Request{
		{Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant},
		{CommandID: "bad command", Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant},
		{CommandID: "read-1", Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"},
	} {
		var failure *common.Failure
		err := ValidateRequest(ctx, request)
		if !errors.As(err, &failure) || failure.Category != common.FailureInvalidRequest || failure.InvalidComponent != common.InvalidComponentCommandID {
			t.Fatalf("ValidateRequest(%#v) = %#v, want invalid command identity", request, err)
		}
	}
}

func TestGrantMutationCanBindExpectedResourceIdentity(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := Request{CommandID: "delete-1", Operation: DeleteGrant, ResourceType: "folder", ResourceID: "folder-1", GrantID: "grant-1"}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	first, err := CommandDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	request.ResourceID = "folder-2"
	if second, err := CommandDigest(request); err != nil || second == first {
		t.Fatalf("resource-bound digest=%q err=%v", second, err)
	}
}

func TestResourceAccessResponseIsBoundToRequestAuthority(t *testing.T) {
	receipts := Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1}
	grant := Grant{ID: "grant-1", ResourceType: "folder", ResourceID: "folder-1", PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "View"}
	duplicatePrincipal := grant
	duplicatePrincipal.ID = "grant-2"
	duplicatePrincipal.AccessLevel = "Manage"
	input := GrantInput{PrincipalID: grant.PrincipalID, PrincipalType: grant.PrincipalType, AccessLevel: grant.AccessLevel}
	for _, test := range []struct {
		name     string
		request  Request
		response Response
		valid    bool
	}{
		{name: "list grants", request: Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"}, response: Response{Grants: []Grant{grant}, Receipts: receipts}, valid: true},
		{name: "get grant", request: Request{Operation: GetGrant, ResourceType: "folder", GrantID: "grant-1"}, response: Response{Grants: []Grant{grant}, Receipts: receipts}, valid: true},
		{name: "ensure grant", request: Request{CommandID: "ensure-1", Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &input}, response: Response{CommandID: "ensure-1", Grants: []Grant{grant}, Receipts: receipts}, valid: true},
		{name: "replace empty", request: Request{CommandID: "replace-1", Operation: ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1"}, response: Response{CommandID: "replace-1", Receipts: receipts}, valid: true},
		{name: "delete has no grants", request: Request{CommandID: "delete-1", Operation: DeleteGrant, ResourceType: "folder", GrantID: "grant-1"}, response: Response{CommandID: "delete-1", Receipts: receipts}, valid: true},
		{name: "bounded ownership transfer", request: Request{CommandID: "transfer-1", Operation: TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2"}, response: Response{CommandID: "transfer-1", AffectedResources: MaxOwnershipTransferResources, Receipts: receipts}, valid: true},
		{name: "ownership transfer over bound", request: Request{CommandID: "transfer-1", Operation: TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2"}, response: Response{CommandID: "transfer-1", AffectedResources: MaxOwnershipTransferResources + 1, Receipts: receipts}},
		{name: "non-transfer smuggles affected count", request: Request{CommandID: "delete-1", Operation: DeleteGrant, ResourceType: "folder", GrantID: "grant-1"}, response: Response{CommandID: "delete-1", AffectedResources: 1, Receipts: receipts}},
		{name: "invalid operation", request: Request{Operation: Operation("READ_ALL_ACL"), ResourceType: "folder", ResourceID: "folder-1"}, response: Response{Grants: []Grant{grant}, Receipts: receipts}},
		{name: "list cross resource", request: Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-2"}, response: Response{Grants: []Grant{grant}, Receipts: receipts}},
		{name: "list duplicate typed principal", request: Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"}, response: Response{Grants: []Grant{grant, duplicatePrincipal}, Receipts: receipts}},
		{name: "get different grant", request: Request{Operation: GetGrant, ResourceType: "folder", GrantID: "grant-2"}, response: Response{Grants: []Grant{grant}, Receipts: receipts}},
		{name: "ensure different principal", request: Request{CommandID: "ensure-1", Operation: EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &GrantInput{PrincipalID: "user-2", PrincipalType: "User", AccessLevel: "View"}}, response: Response{CommandID: "ensure-1", Grants: []Grant{grant}, Receipts: receipts}},
		{name: "delete smuggles grant", request: Request{CommandID: "delete-1", Operation: DeleteGrant, ResourceType: "folder", GrantID: "grant-1"}, response: Response{CommandID: "delete-1", Grants: []Grant{grant}, Receipts: receipts}},
		{name: "wrong mutation command", request: Request{CommandID: "delete-1", Operation: DeleteGrant, ResourceType: "folder", GrantID: "grant-1"}, response: Response{CommandID: "delete-2", Receipts: receipts}},
		{name: "read smuggles command", request: Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"}, response: Response{CommandID: "read-1", Grants: []Grant{grant}, Receipts: receipts}},
	} {
		t.Run(test.name, func(t *testing.T) {
			err := ValidateResponse(test.request, test.response)
			if test.valid && err != nil {
				t.Fatal(err)
			}
			if !test.valid {
				var failure *common.Failure
				if !errors.As(err, &failure) || failure.Category != common.FailureInternal {
					t.Fatalf("ValidateResponse() = %#v, want internal failure", err)
				}
			}
		})
	}
}

func TestResourceAccessCommandDigestBindsEffectNotRetryFence(t *testing.T) {
	revision1, revision2 := uint64(1), uint64(2)
	grants := []GrantInput{
		{PrincipalID: "user-2", PrincipalType: "User", AccessLevel: "View"},
		{PrincipalID: "group-1", PrincipalType: "Group", AccessLevel: "Manage"},
	}
	first := Request{CommandID: "replace-1", Operation: ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: grants, MinCatalogRevision: &revision1}
	second := first
	second.CommandID = "replace-2"
	second.MinCatalogRevision = &revision2
	second.Grants = []GrantInput{grants[1], grants[0]}
	firstDigest, err := CommandDigest(first)
	if err != nil {
		t.Fatal(err)
	}
	secondDigest, err := CommandDigest(second)
	if err != nil {
		t.Fatal(err)
	}
	if !regexp.MustCompile(`^sha256:[0-9a-f]{64}$`).MatchString(firstDigest) || firstDigest != secondDigest {
		t.Fatalf("equivalent effects changed digest: %q != %q", firstDigest, secondDigest)
	}
	second.Grants[0].AccessLevel = "View"
	if changed, err := CommandDigest(second); err != nil || changed == firstDigest {
		t.Fatalf("changed effect digest = %q, %v", changed, err)
	}
	if digest, err := CommandDigest(Request{Operation: ListGrants, ResourceType: "folder", ResourceID: "folder-1"}); err == nil || digest != "" {
		t.Fatalf("read digest = %q, %v; want rejected", digest, err)
	}
}

func TestResourceAccessCommandDigestCanonicalizesSingletonCreate(t *testing.T) {
	grant := GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "View"}
	singleton := Request{CommandID: "create-single", Operation: CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant}
	batch := Request{CommandID: "create-batch", Operation: CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grants: []GrantInput{grant}}

	singletonDigest, err := CommandDigest(singleton)
	if err != nil {
		t.Fatal(err)
	}
	batchDigest, err := CommandDigest(batch)
	if err != nil {
		t.Fatal(err)
	}
	if singletonDigest != batchDigest {
		t.Fatalf("equivalent CREATE_GRANT effects changed digest: %q != %q", singletonDigest, batchDigest)
	}
}
