package effectivepermissionsv1

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

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

func TestEffectivePermissionsValidationAcceptsCompleteSortedProjection(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := Request{TargetPrincipalID: "principal-2"}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() error = %v", err)
	}
	if err := ValidateResponse(validResponse()); err != nil {
		t.Fatalf("ValidateResponse() error = %v", err)
	}
}

func TestEffectivePermissionsRequestFailsClosed(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	canceled, cancelCanceled := context.WithCancel(context.Background())
	cancelCanceled()
	expired, cancelExpired := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
	defer cancelExpired()
	tests := []struct {
		name      string
		ctx       context.Context
		request   Request
		category  FailureCategory
		component InvalidComponent
	}{
		{name: "missing deadline", ctx: context.Background(), category: FailureInvalidRequest, component: InvalidComponentDeadline},
		{name: "canceled", ctx: canceled, category: FailureCanceled},
		{name: "expired", ctx: expired, category: FailureDeadlineExceeded},
		{name: "whitespace target", ctx: ctx, request: Request{TargetPrincipalID: " principal"}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "control target", ctx: ctx, request: Request{TargetPrincipalID: "principal\n2"}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "oversized target", ctx: ctx, request: Request{TargetPrincipalID: strings.Repeat("p", MaxPrincipalIdentifierBytes+1)}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "empty selection", ctx: ctx, request: Request{Selection: &Selection{}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "unsorted sections", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionFieldPermissions, SectionObjectPermissions}, ResourceTypes: []string{"Account"}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "duplicate sections", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionObjectPermissions, SectionObjectPermissions}, ResourceTypes: []string{"Account"}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "resource section without resources", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionObjectPermissions}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "resources without resource section", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionSystemPermissions}, ResourceTypes: []string{"Account"}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "unsorted resources", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionObjectPermissions}, ResourceTypes: []string{"Contact", "Account"}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "case duplicate resources", ctx: ctx, request: Request{Selection: &Selection{Sections: []Section{SectionObjectPermissions}, ResourceTypes: []string{"Account", "account"}}}, category: FailureInvalidRequest, component: InvalidComponentOperation},
		{name: "zero minimum Catalog revision", ctx: ctx, request: Request{MinCatalogRevision: new(uint64)}, category: FailureInvalidRequest, component: InvalidComponentOperation},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var failure *Failure
			err := ValidateRequest(test.ctx, test.request)
			if !errors.As(err, &failure) ||
				failure.Category != test.category ||
				failure.InvalidComponent != test.component {
				t.Fatalf("error = %#v, want %s/%s", err, test.category, test.component)
			}
		})
	}
}

func TestEffectivePermissionsSelectedResponseIsClosedToRequest(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := Request{Selection: &Selection{
		Sections: []Section{
			SectionObjectPermissions,
			SectionFieldPermissions,
			SectionRecordTypePermissions,
		},
		ResourceTypes: []string{"Account"},
	}}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() error = %v", err)
	}
	selected := validResponse()
	selected.SystemPermissions = nil
	selected.CustomPermissions = nil
	selected.ApplicationPermissions = nil
	selected.NavigationPermissions = nil
	if err := ValidateResponseForRequest(request, selected); err != nil {
		t.Fatalf("ValidateResponseForRequest() error = %v", err)
	}
	minimum := selected.Receipts.CatalogRevision + 1
	staleRequest := request
	staleRequest.MinCatalogRevision = &minimum
	if err := ValidateResponseForRequest(staleRequest, selected); err == nil {
		t.Fatal("ValidateResponseForRequest() accepted stale Catalog receipts")
	}
	invalidRequest := request
	invalidRequest.Selection = &Selection{}
	if err := ValidateResponseForRequest(invalidRequest, selected); err == nil {
		t.Fatal("ValidateResponseForRequest() accepted invalid request shape")
	}

	tests := []struct {
		name   string
		mutate func(*Response)
	}{
		{name: "unrequested section", mutate: func(response *Response) {
			response.SystemPermissions = []PermissionGrant{{PermissionID: "ViewSetup"}}
		}},
		{name: "unselected object", mutate: func(response *Response) {
			response.ObjectPermissions = []ObjectPermission{{ResourceType: "Contact", CanRead: true}}
		}},
		{name: "unselected field object", mutate: func(response *Response) {
			response.FieldPermissions = []FieldPermission{{ResourceType: "Contact", FieldName: "Name", CanRead: true}}
		}},
		{name: "unselected record type object", mutate: func(response *Response) {
			response.RecordTypePermissions = []RecordTypePermission{{
				ResourceType: "Contact", RecordTypeID: "rt-contact", Available: true,
			}}
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			response := selected
			test.mutate(&response)
			if err := ValidateResponseForRequest(request, response); err == nil {
				t.Fatal("ValidateResponseForRequest() accepted authority outside selection")
			}
		})
	}
}

func TestEffectivePermissionsResponseRejectsAmbiguousOrMalformedProjection(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*Response)
	}{
		{name: "wrong receipt", mutate: func(response *Response) {
			response.Receipts.CapabilityVersion = "other"
		}},
		{name: "unsorted system", mutate: func(response *Response) {
			response.SystemPermissions = []PermissionGrant{{PermissionID: "Z"}, {PermissionID: "A"}}
		}},
		{name: "duplicate custom", mutate: func(response *Response) {
			response.CustomPermissions = []PermissionGrant{{PermissionID: "Export"}, {PermissionID: "Export"}}
		}},
		{name: "unsorted objects", mutate: func(response *Response) {
			response.ObjectPermissions = []ObjectPermission{{ResourceType: "Contact"}, {ResourceType: "Account"}}
		}},
		{name: "case duplicate objects", mutate: func(response *Response) {
			response.ObjectPermissions = []ObjectPermission{{ResourceType: "Account"}, {ResourceType: "account"}}
		}},
		{name: "duplicate fields", mutate: func(response *Response) {
			response.FieldPermissions = []FieldPermission{
				{ResourceType: "Account", FieldName: "Name"},
				{ResourceType: "account", FieldName: "name"},
			}
		}},
		{name: "invalid field", mutate: func(response *Response) {
			response.FieldPermissions[0].FieldName = "Owner.Name"
		}},
		{name: "control permission", mutate: func(response *Response) {
			response.SystemPermissions[0].PermissionID = "View\nSetup"
		}},
		{name: "unsorted applications", mutate: func(response *Response) {
			response.ApplicationPermissions = []ApplicationPermission{
				{ApplicationID: "Service"}, {ApplicationID: "Sales"},
			}
		}},
		{name: "case duplicate navigation", mutate: func(response *Response) {
			response.NavigationPermissions = []NavigationPermission{
				{NavigationKey: "Account"}, {NavigationKey: "account"},
			}
		}},
		{name: "hidden default application", mutate: func(response *Response) {
			response.ApplicationPermissions[0].CanView = false
		}},
		{name: "unsorted record types", mutate: func(response *Response) {
			response.RecordTypePermissions = []RecordTypePermission{
				{ResourceType: "Contact", RecordTypeID: "rt-contact"},
				{ResourceType: "Account", RecordTypeID: "rt-account"},
			}
		}},
		{name: "duplicate record-type id", mutate: func(response *Response) {
			response.RecordTypePermissions = append(response.RecordTypePermissions, response.RecordTypePermissions[0])
		}},
		{name: "unavailable default record type", mutate: func(response *Response) {
			response.RecordTypePermissions[0].Available = false
		}},
		{name: "multiple defaults for object", mutate: func(response *Response) {
			response.RecordTypePermissions = append(response.RecordTypePermissions, RecordTypePermission{
				ResourceType: "Account", RecordTypeID: "rt-consumer", Available: true, IsDefault: true,
			})
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			response := validResponse()
			test.mutate(&response)
			if err := ValidateResponse(response); err == nil {
				t.Fatal("ValidateResponse() accepted malformed projection")
			}
		})
	}
}

func TestEffectivePermissionsResponseIsBounded(t *testing.T) {
	response := validResponse()
	response.SystemPermissions = make([]PermissionGrant, MaxSystemPermissions+1)
	for index := range response.SystemPermissions {
		response.SystemPermissions[index] = PermissionGrant{PermissionID: "P"}
	}
	var failure *Failure
	err := ValidateResponse(response)
	if !errors.As(err, &failure) ||
		failure.Category != FailureResourceExhausted ||
		failure.LimitKind != LimitOperationCount ||
		failure.Maximum != MaxSystemPermissions {
		t.Fatalf("error = %#v, want system-permission RESOURCE_EXHAUSTED", err)
	}
}

func TestEffectivePermissionsRecordTypeProjectionIsBounded(t *testing.T) {
	response := validResponse()
	response.RecordTypePermissions = make([]RecordTypePermission, MaxRecordTypePermissions+1)
	var failure *Failure
	err := ValidateResponse(response)
	if !errors.As(err, &failure) ||
		failure.Category != FailureResourceExhausted ||
		failure.LimitKind != LimitOperationCount ||
		failure.Maximum != MaxRecordTypePermissions {
		t.Fatalf("error = %#v, want record-type RESOURCE_EXHAUSTED", err)
	}
}

func validResponse() Response {
	return Response{
		SystemPermissions: []PermissionGrant{{PermissionID: "ApiEnabled", Granted: true}},
		CustomPermissions: []PermissionGrant{{PermissionID: "CanExport", Granted: false}},
		ObjectPermissions: []ObjectPermission{{
			ResourceType: "Account", CanRead: true, CanCreate: true,
		}},
		FieldPermissions: []FieldPermission{{
			ResourceType: "Account", FieldName: "Name", CanRead: true,
		}},
		ApplicationPermissions: []ApplicationPermission{{
			ApplicationID: "Sales", CanView: true, IsDefault: true,
		}},
		NavigationPermissions: []NavigationPermission{{
			NavigationKey: "Account", CanView: true,
		}},
		RecordTypePermissions: []RecordTypePermission{{
			ResourceType: "Account", RecordTypeID: "rt-business",
			Available: true, IsDefault: true,
		}},
		Receipts: common.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: CapabilityName,
			CatalogRevision: 3, SchemaGeneration: 4, EngineGeneration: 5,
			SecurityEpoch: 6,
		},
	}
}
