package runtimeapi

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

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	delegationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/delegationv1"
	pagelayoutsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

type pageLayoutRoleHierarchyStub struct {
	t         *testing.T
	want      string
	wantEpoch uint64
	parents   []string
	err       error
}

type pageLayoutResolverStub struct {
	view *catalogsnapshot.RuntimeMetadataView
}

func (stub pageLayoutResolverStub) ResolveCurrentPageLayouts(
	_ context.Context,
	objectID string,
	profileID string,
	roles []string,
	recordTypeIDs []string,
) (map[string]*models.PageLayout, error) {
	result := make(map[string]*models.PageLayout, len(recordTypeIDs))
	for _, recordTypeID := range recordTypeIDs {
		layout, _, err := stub.view.ResolvePageLayout(objectID, profileID, roles, recordTypeID)
		if err != nil {
			return nil, err
		}
		result[recordTypeID] = layout
	}
	return result, nil
}

func (stub pageLayoutResolverStub) ReadCurrentObjectListFields(
	_ context.Context,
	objectIDs []string,
) (map[string][]string, error) {
	result := make(map[string][]string, len(objectIDs))
	for _, objectID := range objectIDs {
		result[strings.ToLower(objectID)] = []string{}
	}
	return result, nil
}

type fixedPageLayoutResolver struct{ layout *models.PageLayout }

func (stub fixedPageLayoutResolver) ResolveCurrentPageLayouts(
	_ context.Context, _ string, _ string, _ []string, recordTypeIDs []string,
) (map[string]*models.PageLayout, error) {
	result := make(map[string]*models.PageLayout, len(recordTypeIDs))
	for _, recordTypeID := range recordTypeIDs {
		result[recordTypeID] = stub.layout
	}
	return result, nil
}

func (stub fixedPageLayoutResolver) ReadCurrentObjectListFields(
	_ context.Context,
	objectIDs []string,
) (map[string][]string, error) {
	result := make(map[string][]string, len(objectIDs))
	for _, objectID := range objectIDs {
		result[strings.ToLower(objectID)] = []string{"Name", "OwnerId"}
	}
	return result, nil
}

func TestPageLayoutResolutionServerUsesCurrentDatabaseLayoutInsteadOfCatalogLayout(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t, false)
	current := &models.PageLayout{
		ID: "layout-saved-now", ObjectAPIName: "WorkItem", LayoutName: "Saved now",
		CompactLayout: []string{}, Sections: []models.PageSection{}, RelatedLists: []models.RelatedListConfig{},
		HeaderActions: []models.ActionConfig{}, QuickActions: []models.ActionConfig{},
	}
	server, err := NewPageLayoutResolutionServer(fixedPageLayoutResolver{layout: current}, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContextWithRole(t, view, nil)
	request := pagelayoutsemantic.Request{ObjectID: "WorkItem"}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, err := server.ResolvePageLayout(ctx, wireRequest)
	if err != nil {
		t.Fatal(err)
	}
	response, err := mapping.FromWireResolvePageLayoutResponse(request, wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Selections) != 1 || response.Selections[0].LayoutID != current.ID ||
		len(response.Layouts) != 1 || response.Layouts[0].LayoutID != current.ID {
		t.Fatalf("response used stale Catalog Layout: %#v", response)
	}
}

func TestPageLayoutResolutionServerReadsCurrentListFieldsWithoutLayoutAssignment(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t, false)
	server, err := NewPageLayoutResolutionServer(fixedPageLayoutResolver{}, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContextWithRole(t, view, nil)
	request := pagelayoutsemantic.ObjectListFieldsRequest{ObjectIDs: []string{"WorkItem"}}
	wireRequest, err := mapping.ToWireReadObjectListFieldsRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, err := server.ReadObjectListFields(ctx, wireRequest)
	if err != nil {
		t.Fatal(err)
	}
	response, err := mapping.FromWireReadObjectListFieldsResponse(request, wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Objects) != 1 ||
		!reflect.DeepEqual(response.Objects[0].OrderedFieldIDs, []string{"Name", "OwnerId"}) {
		t.Fatalf("current ListFields response = %#v", response)
	}
}

func (stub pageLayoutRoleHierarchyStub) GetRoleAncestorsAtSecurityEpoch(
	_ context.Context,
	orgID, roleID string,
	securityEpoch uint64,
) ([]string, error) {
	stub.t.Helper()
	if orgID != testOrgID {
		stub.t.Fatalf("role hierarchy org = %q, want %q", orgID, testOrgID)
	}
	if roleID != stub.want {
		stub.t.Fatalf("role hierarchy read = %q, want %q", roleID, stub.want)
	}
	if securityEpoch != stub.wantEpoch {
		stub.t.Fatalf("role hierarchy security epoch = %d, want %d", securityEpoch, stub.wantEpoch)
	}
	return append([]string(nil), stub.parents...), stub.err
}

func TestPageLayoutResolutionServerUsesAuthenticatedProfileAndRole(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t)
	server, err := NewPageLayoutResolutionServer(pageLayoutResolverStub{view: view}, pageLayoutRoleHierarchyStub{
		t: t, want: "role-child", wantEpoch: 3, parents: []string{"role-near", "role-far"},
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContext(t, view)
	recordTypeID := "rt-enterprise"
	secondaryRecordTypeID := "rt-secondary"
	request := pagelayoutsemantic.Request{
		ObjectID: "WorkItem", RecordTypeIDs: []string{recordTypeID, secondaryRecordTypeID},
	}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, err := server.ResolvePageLayout(ctx, wireRequest)
	if err != nil {
		t.Fatal(err)
	}
	response, err := mapping.FromWireResolvePageLayoutResponse(request, wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Selections) != 2 || response.Selections[0].RecordTypeID == nil ||
		*response.Selections[0].RecordTypeID != recordTypeID ||
		response.Selections[0].LayoutID != "layout-profile-record-type" ||
		response.Selections[1].RecordTypeID == nil || *response.Selections[1].RecordTypeID != secondaryRecordTypeID ||
		response.Selections[1].LayoutID != "layout-role-near" || len(response.Layouts) != 2 ||
		response.Receipts.CapabilityVersion != pagelayoutsemantic.CapabilityName {
		t.Fatalf("caller-effective response = %#v", response)
	}

	request = pagelayoutsemantic.Request{ObjectID: "WorkItem"}
	wireRequest, err = mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, err = server.ResolvePageLayout(ctx, wireRequest)
	if err != nil {
		t.Fatal(err)
	}
	response, err = mapping.FromWireResolvePageLayoutResponse(request, wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Selections) != 1 || response.Selections[0].RecordTypeID != nil ||
		response.Selections[0].LayoutID != "layout-role-near" {
		t.Fatalf("nearest-role response = %#v", response)
	}
}

func TestPageLayoutResolutionServerFailsClosedWhenRoleHierarchyEpochIsUnavailable(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t)
	server, err := NewPageLayoutResolutionServer(pageLayoutResolverStub{view: view}, pageLayoutRoleHierarchyStub{
		t: t, want: "role-child", wantEpoch: 3, err: errors.New("stale hierarchy"),
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContext(t, view)
	request := pagelayoutsemantic.Request{ObjectID: "WorkItem"}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	_, wireErr := server.ResolvePageLayout(ctx, wireRequest)
	var failure *pagelayoutsemantic.Failure
	decoded := mapping.FromPlatformStatusError(wireErr, pagelayoutsemantic.CapabilityName)
	if !errors.As(decoded, &failure) || failure.Category != pagelayoutsemantic.FailureUnavailable {
		t.Fatalf("stale role hierarchy = %v", decoded)
	}
}

func TestPageLayoutResolutionServerUsesCurrentRoleAuthorityForRoleBearingUser(t *testing.T) {
	request := pagelayoutsemantic.Request{ObjectID: "WorkItem"}

	withoutRoles := pageLayoutResolutionServerTestView(t, false)
	server, err := NewPageLayoutResolutionServer(
		pageLayoutResolverStub{view: withoutRoles},
		pageLayoutRoleHierarchyStub{t: t, want: "role-child", wantEpoch: 3},
	)
	if err != nil {
		t.Fatal(err)
	}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(
		pageLayoutResolutionServerTestContext(t, withoutRoles), request,
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := server.ResolvePageLayout(
		pageLayoutResolutionServerTestContext(t, withoutRoles), wireRequest,
	); err != nil {
		t.Fatalf("profile-only view required role authority: %v", err)
	}

	withRoles := pageLayoutResolutionServerTestView(t)
	server, err = NewPageLayoutResolutionServer(pageLayoutResolverStub{view: withRoles}, nil)
	if err != nil {
		t.Fatal(err)
	}
	wireRequest, err = mapping.ToWireResolvePageLayoutRequest(
		pageLayoutResolutionServerTestContext(t, withRoles), request,
	)
	if err != nil {
		t.Fatal(err)
	}
	_, wireErr := server.ResolvePageLayout(
		pageLayoutResolutionServerTestContext(t, withRoles), wireRequest,
	)
	var failure *pagelayoutsemantic.Failure
	decoded := mapping.FromPlatformStatusError(wireErr, pagelayoutsemantic.CapabilityName)
	if !errors.As(decoded, &failure) || failure.Category != pagelayoutsemantic.FailureUnavailable {
		t.Fatalf("role-based view without authority = %v", decoded)
	}
}

func TestPageLayoutResolutionServerDoesNotNeedRoleAuthorityForUserWithoutRole(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t)
	server, err := NewPageLayoutResolutionServer(pageLayoutResolverStub{view: view}, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContextWithRole(t, view, nil)
	request := pagelayoutsemantic.Request{ObjectID: "WorkItem"}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, wireErr := server.ResolvePageLayout(ctx, wireRequest)
	if wireErr != nil {
		t.Fatal(wireErr)
	}
	response, err := mapping.FromWireResolvePageLayoutResponse(request, wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Selections) != 1 || response.Selections[0].LayoutID != "layout-default" {
		t.Fatalf("role-less response = %#v", response)
	}
}

func TestPageLayoutResolutionServerRejectsMissingTrustedIdentity(t *testing.T) {
	view := pageLayoutResolutionServerTestView(t)
	server, err := NewPageLayoutResolutionServer(pageLayoutResolverStub{view: view}, pageLayoutRoleHierarchyStub{t: t})
	if err != nil {
		t.Fatal(err)
	}
	ctx := pageLayoutResolutionServerTestContext(t, view)
	ctx = context.WithValue(ctx, trustedCallContextKey{}, (*TrustedCall)(nil))
	request := pagelayoutsemantic.Request{ObjectID: "WorkItem"}
	wireRequest, err := mapping.ToWireResolvePageLayoutRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	_, wireErr := server.ResolvePageLayout(ctx, wireRequest)
	var failure *pagelayoutsemantic.Failure
	decoded := mapping.FromPlatformStatusError(wireErr, pagelayoutsemantic.CapabilityName)
	if !errors.As(decoded, &failure) || failure.Category != pagelayoutsemantic.FailureUnavailable {
		t.Fatalf("missing trusted identity = %v", decoded)
	}
}

func pageLayoutResolutionServerTestView(t *testing.T, includeRoleAssignments ...bool) *catalogsnapshot.RuntimeMetadataView {
	t.Helper()
	const revision catalogsnapshot.CatalogRevision = 17
	object := &models.ObjectMetadata{
		ID: "object-work-item", APIName: "WorkItem", Label: "Work Item", PluralLabel: "Work Items",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{{
			ID: "work-item-id", APIName: constants.FieldID, Label: "ID",
			Type: constants.FieldTypeString, PhysicalName: "id", IsSystem: true,
		}},
	}
	layout := func(id string) *models.PageLayout {
		return &models.PageLayout{
			ID: id, ObjectAPIName: "WorkItem", LayoutName: id,
			CompactLayout: []string{}, Sections: []models.PageSection{},
			RelatedLists: []models.RelatedListConfig{}, HeaderActions: []models.ActionConfig{},
			QuickActions: []models.ActionConfig{},
		}
	}
	profile, recordType := "profile-session", "rt-enterprise"
	secondaryRecordType := "rt-secondary"
	nearRole, farRole := "role-near", "role-far"
	assignments := []catalogsnapshot.LayoutAssignmentInput{
		{CatalogRevision: revision, Assignment: &models.LayoutAssignment{ID: "assignment-default", ObjectAPIName: "WorkItem", LayoutID: "layout-default", Specificity: 0}},
		{CatalogRevision: revision, Assignment: &models.LayoutAssignment{ID: "assignment-profile-record-type", ObjectAPIName: "WorkItem", LayoutID: "layout-profile-record-type", ProfileID: &profile, RecordTypeID: &recordType, Specificity: 2}},
	}
	if len(includeRoleAssignments) == 0 || includeRoleAssignments[0] {
		assignments = append(assignments,
			catalogsnapshot.LayoutAssignmentInput{CatalogRevision: revision, Assignment: &models.LayoutAssignment{ID: "assignment-role-near", ObjectAPIName: "WorkItem", LayoutID: "layout-role-near", RoleID: &nearRole, Specificity: 1}},
			catalogsnapshot.LayoutAssignmentInput{CatalogRevision: revision, Assignment: &models.LayoutAssignment{ID: "assignment-role-far", ObjectAPIName: "WorkItem", LayoutID: "layout-role-far", RoleID: &farRole, Specificity: 1}},
		)
	}
	input := catalogsnapshot.CompileInput{
		OrgID: testOrgID,
		Lineage: catalogsnapshot.Lineage{
			CatalogRevision: revision, SchemaGeneration: 8, EngineGeneration: 9,
		},
		Objects: []catalogsnapshot.ObjectInput{{CatalogRevision: revision, Metadata: object}},
		PageLayouts: []catalogsnapshot.PageLayoutInput{
			{CatalogRevision: revision, Layout: layout("layout-default")},
			{CatalogRevision: revision, Layout: layout("layout-role-near")},
			{CatalogRevision: revision, Layout: layout("layout-role-far")},
			{CatalogRevision: revision, Layout: layout("layout-profile-record-type")},
		},
		LayoutAssignments: assignments,
		PageLayoutRecordTypes: []catalogsnapshot.PageLayoutRecordTypeInput{
			{CatalogRevision: revision, ObjectAPIName: "WorkItem", RecordTypeID: recordType},
			{CatalogRevision: revision, ObjectAPIName: "WorkItem", RecordTypeID: secondaryRecordType},
		},
		Projections: []catalogsnapshot.RuntimeProjectionInput{{
			CatalogRevision: revision, AppInstallationID: testInstallationID,
			CapabilityID:   pagelayoutsemantic.CapabilityName,
			Profile:        catalogsnapshot.ProjectionPageLayoutResolutionV1,
			ObjectAPINames: []string{"WorkItem"},
		}},
	}
	artifact, err := catalogsnapshot.CompileCatalogSnapshot(input)
	if err != nil {
		t.Fatal(err)
	}
	snapshot, err := catalogsnapshot.OpenCatalogSnapshot(artifact)
	if err != nil {
		t.Fatal(err)
	}
	projected, err := catalogsnapshot.ProjectRuntimeMetadataView(snapshot, catalogsnapshot.ProjectionKey{
		AppInstallationID: testInstallationID, CapabilityID: pagelayoutsemantic.CapabilityName,
	})
	if err != nil {
		t.Fatal(err)
	}
	view, err := catalogsnapshot.OpenRuntimeMetadataView(projected)
	if err != nil {
		t.Fatal(err)
	}
	return view
}

func pageLayoutResolutionServerTestContext(
	t *testing.T,
	view *catalogsnapshot.RuntimeMetadataView,
) context.Context {
	roleID := "role-child"
	return pageLayoutResolutionServerTestContextWithRole(t, view, &roleID)
}

func pageLayoutResolutionServerTestContextWithRole(
	t *testing.T,
	view *catalogsnapshot.RuntimeMetadataView,
	roleID *string,
) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	lineage := view.Lineage()
	ctx = context.WithValue(ctx, runtimeExecutionScopeContextKey{}, RuntimeExecutionScope{
		view: view, orgID: testOrgID, appInstallationID: testInstallationID,
		workloadID: testWorkloadID, capability: pagelayoutsemantic.CapabilityName,
		receipts: pagelayoutsemantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: pagelayoutsemantic.CapabilityName,
			CatalogRevision: uint64(lineage.CatalogRevision), SchemaGeneration: uint64(lineage.SchemaGeneration),
			EngineGeneration: uint64(lineage.EngineGeneration), SecurityEpoch: 3,
		},
	})
	return context.WithValue(ctx, trustedCallContextKey{}, newTrustedCall(delegationv1.Claims{
		DelegationID: "delegation-user-1", OrgID: testOrgID, PrincipalID: "user-1",
		SessionID: "session-user-1", CallerWorkloadID: testWorkloadID,
		AppInstallationID: testInstallationID, RuntimeAudience: "runtime-test",
		Capabilities: []string{pagelayoutsemantic.CapabilityName}, AuthEpoch: 2, SecurityEpoch: 3,
	}, models.UserSession{ID: "user-1", ProfileID: "profile-session", RoleID: roleID}))
}
