package permissions

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

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
)

func TestDefaultPermissionAuthoringPlannerFreezesRecordsValidationAsInvalidChange(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	records := &permissionAuthoringRecordsPlannerForTest{
		writes:  &repo.writes,
		planErr: appErrors.NewValidationError("SobjectType", "unknown object Missing__c"),
	}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(&PermissionService{repo: repo}, records)
	if err != nil {
		t.Fatal(err)
	}
	_, err = planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringAdditivePermissionSet),
		permissionauthoringv1.Changes{Objects: []permissionauthoringv1.ObjectGrantChange{{ResourceType: "Missing__c", Read: true}}},
		&models.UserSession{ID: "admin-1"},
	)
	var rejection *PermissionAuthoringRejection
	if !errors.As(err, &rejection) || rejection.Code != permissionauthoringv1.RejectionInvalidChange {
		t.Fatalf("PlanPermissionAuthoringMutation() = %T(%v), want INVALID_CHANGE", err, err)
	}
}

func TestDefaultPermissionAuthoringPlannerValidatesAllFamiliesBeforeWrites(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	records := &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(&PermissionService{
		repo: repo,
	}, records)
	if err != nil {
		t.Fatal(err)
	}
	changes := permissionAuthoringRequestForTest(t).Changes

	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringAdditivePermissionSet), changes, nil,
	)
	if err != nil {
		t.Fatalf("PlanPermissionAuthoringMutation(): %v", err)
	}
	if !plan.Changed() {
		t.Fatal("plan reports no change")
	}
	if len(repo.writes) != 0 {
		t.Fatalf("planning produced writes: %v", repo.writes)
	}
	if err := plan.Apply(t.Context()); err != nil {
		t.Fatalf("Apply(): %v", err)
	}
	want := []string{"system:ManageUsers", "object:Account", "field:Account.Name", "record-type:record-type-1", "custom:custom-permission-1"}
	if strings.Join(repo.writes, "|") != strings.Join(want, "|") {
		t.Fatalf("writes = %v, want %v", repo.writes, want)
	}
}

func TestDefaultPermissionAuthoringPlannerLateFamilyRejectionLeavesEveryFamilyUnwritten(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	records := &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(&PermissionService{
		repo: repo,
	}, records)
	if err != nil {
		t.Fatal(err)
	}
	changes := permissionAuthoringRequestForTest(t).Changes
	changes.Custom[0].CapabilityID = "missing-custom-permission"

	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringAdditivePermissionSet), changes, nil,
	)
	var rejection *PermissionAuthoringRejection
	if plan != nil || !errors.As(err, &rejection) || rejection.Code != permissionauthoringv1.RejectionInvalidChange {
		t.Fatalf("PlanPermissionAuthoringMutation() = %#v, %v", plan, err)
	}
	if len(repo.writes) != 0 {
		t.Fatalf("late-family validation leaked early writes: %v", repo.writes)
	}
}

func permissionAuthoringAggregateForTest(role ports.PermissionAuthoringAggregateRole) ports.LockedPermissionAuthoringAggregate {
	return ports.LockedPermissionAuthoringAggregate{
		CanonicalPermissionSetID: "permission-set-1",
		Role:                     role,
		Revision:                 7,
	}
}

func TestDefaultPermissionAuthoringPlannerDoesNotMaterializeAbsentFalseGrants(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	records := &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(&PermissionService{
		repo: repo,
	}, records)
	if err != nil {
		t.Fatal(err)
	}
	changes := permissionAuthoringRequestForTest(t).Changes
	changes.System[0].Enabled = false
	changes.Objects[0] = permissionauthoringv1.ObjectGrantChange{ResourceType: "Account"}
	changes.Fields[0] = permissionauthoringv1.FieldGrantChange{ResourceType: "Account", FieldName: "Name"}
	changes.RecordTypes[0].Visible = false
	changes.Custom[0].Enabled = false

	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringAdditivePermissionSet), changes, nil,
	)
	if err != nil {
		t.Fatalf("PlanPermissionAuthoringMutation(): %v", err)
	}
	if plan.Changed() {
		t.Fatal("absent false additive grants were materialized as a semantic change")
	}
	if err := plan.Apply(t.Context()); err != nil {
		t.Fatalf("Apply(noop): %v", err)
	}
	if len(repo.writes) != 0 {
		t.Fatalf("no-op plan produced writes: %v", repo.writes)
	}
}

func TestDefaultPermissionAuthoringPlannerCanonicalizesPhysicalFalseAdditiveRow(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	repo.recordTypeRows = []models.PermissionSetRecordTypeVisibility{{RecordTypeID: "record-type-1", IsVisible: false}}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(
		&PermissionService{repo: repo}, &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes},
	)
	if err != nil {
		t.Fatal(err)
	}
	change := permissionauthoringv1.RecordTypeGrantChange{RecordTypeID: "record-type-1", ResourceType: "Account", Visible: false}
	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringAdditivePermissionSet),
		permissionauthoringv1.Changes{RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{change}}, nil,
	)
	if err != nil {
		t.Fatal(err)
	}
	if !plan.Changed() || len(plan.FinalChanges().RecordTypes) != 1 {
		t.Fatalf("additive false-row canonicalization = changed %t, %+v", plan.Changed(), plan.FinalChanges())
	}
	if err := plan.Apply(t.Context()); err != nil {
		t.Fatal(err)
	}
	if len(repo.recordTypeSets) != 1 || repo.recordTypeSets[0].RecordTypeID != change.RecordTypeID ||
		repo.recordTypeSets[0].Visible != change.Visible {
		t.Fatalf("additive false-row write = %+v", repo.recordTypeSets)
	}
}

func TestDefaultPermissionAuthoringPlannerMaterializesCompleteProfileRecordTypeObject(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	repo.recordTypes = []ports.RecordTypeCatalogEntry{
		{RecordTypeID: "account-a", ObjectAPIName: "Account", Name: "account-a", Label: "account-a", IsActive: true},
		{RecordTypeID: "account-b", ObjectAPIName: "Account", Name: "account-b", Label: "account-b", IsActive: true},
	}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(
		&PermissionService{repo: repo}, &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes},
	)
	if err != nil {
		t.Fatal(err)
	}
	changes := permissionauthoringv1.Changes{RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{
		{RecordTypeID: "account-a", ResourceType: "Account", Visible: false},
		{RecordTypeID: "account-b", ResourceType: "Account", Visible: true},
	}}
	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringProfileBaseline), changes, nil,
	)
	if err != nil {
		t.Fatalf("PlanPermissionAuthoringMutation(): %v", err)
	}
	if !plan.Changed() || len(plan.FinalChanges().RecordTypes) != 2 {
		t.Fatalf("Profile plan = changed %t, changes %+v", plan.Changed(), plan.FinalChanges())
	}
	if err := plan.Apply(t.Context()); err != nil {
		t.Fatal(err)
	}
	if len(repo.recordTypeSets) != 2 ||
		repo.recordTypeSets[0].RecordTypeID != changes.RecordTypes[0].RecordTypeID ||
		repo.recordTypeSets[0].Visible != changes.RecordTypes[0].Visible ||
		repo.recordTypeSets[1].RecordTypeID != changes.RecordTypes[1].RecordTypeID ||
		repo.recordTypeSets[1].Visible != changes.RecordTypes[1].Visible {
		t.Fatalf("Profile writes = %+v, want %+v", repo.recordTypeSets, changes.RecordTypes)
	}
}

func TestDefaultPermissionAuthoringPlannerRejectsIncompleteProfileRecordTypeObjectBeforeWrites(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	repo.recordTypes = []ports.RecordTypeCatalogEntry{
		{RecordTypeID: "account-a", ObjectAPIName: "Account", Name: "account-a", Label: "account-a", IsActive: true},
		{RecordTypeID: "account-b", ObjectAPIName: "Account", Name: "account-b", Label: "account-b", IsActive: true},
	}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(
		&PermissionService{repo: repo}, &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes},
	)
	if err != nil {
		t.Fatal(err)
	}
	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringProfileBaseline),
		permissionauthoringv1.Changes{RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{{RecordTypeID: "account-a", ResourceType: "Account"}}}, nil,
	)
	var rejection *PermissionAuthoringRejection
	if plan != nil || !errors.As(err, &rejection) || rejection.Code != permissionauthoringv1.RejectionConflict {
		t.Fatalf("incomplete Profile plan = %#v, %v", plan, err)
	}
	if len(repo.writes) != 0 {
		t.Fatalf("incomplete Profile command wrote %+v", repo.writes)
	}
}

func TestDefaultPermissionAuthoringPlannerRejectsRecordTypeReparentBeforeWrites(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	repo.recordTypes = []ports.RecordTypeCatalogEntry{{
		RecordTypeID: "record-type-1", ObjectAPIName: "Contact",
		Name: "record-type-1", Label: "record-type-1", IsActive: true,
	}}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(
		&PermissionService{repo: repo}, &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes},
	)
	if err != nil {
		t.Fatal(err)
	}
	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringProfileBaseline),
		permissionauthoringv1.Changes{RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{{
			RecordTypeID: "record-type-1", ResourceType: "Account", Visible: true,
		}}}, nil,
	)
	var rejection *PermissionAuthoringRejection
	if plan != nil || !errors.As(err, &rejection) || rejection.Code != permissionauthoringv1.RejectionConflict {
		t.Fatalf("reparented Profile plan = %#v, %v", plan, err)
	}
	if len(repo.writes) != 0 {
		t.Fatalf("reparented Profile command wrote %+v", repo.writes)
	}
}

func TestDefaultPermissionAuthoringPlannerCanonicalizesMissingSiblingOnManagedProfileObject(t *testing.T) {
	repo := permissionAuthoringPlannerRepoForTest()
	repo.recordTypes = []ports.RecordTypeCatalogEntry{
		{RecordTypeID: "account-a", ObjectAPIName: "Account", Name: "account-a", Label: "account-a", IsActive: true},
		{RecordTypeID: "account-b", ObjectAPIName: "Account", Name: "account-b", Label: "account-b", IsActive: true},
	}
	repo.recordTypeRows = []models.PermissionSetRecordTypeVisibility{{RecordTypeID: "account-a", IsVisible: false}}
	planner, err := NewDefaultPermissionAuthoringMutationPlanner(
		&PermissionService{repo: repo}, &permissionAuthoringRecordsPlannerForTest{writes: &repo.writes},
	)
	if err != nil {
		t.Fatal(err)
	}
	plan, err := planner.PlanPermissionAuthoringMutation(
		t.Context(), permissionAuthoringAggregateForTest(ports.PermissionAuthoringProfileBaseline),
		permissionauthoringv1.Changes{RecordTypes: []permissionauthoringv1.RecordTypeGrantChange{
			{RecordTypeID: "account-a", ResourceType: "Account", Visible: false}, {RecordTypeID: "account-b", ResourceType: "Account", Visible: false},
		}}, nil,
	)
	if err != nil {
		t.Fatal(err)
	}
	if got := plan.FinalChanges().RecordTypes; len(got) != 1 || got[0].RecordTypeID != "account-b" || got[0].Visible {
		t.Fatalf("canonical Profile delta = %+v", got)
	}
}

type permissionAuthoringPlannerRepo struct {
	ports.PermissionRepository
	writes         []string
	recordTypes    []ports.RecordTypeCatalogEntry
	recordTypeRows []models.PermissionSetRecordTypeVisibility
	recordTypeSets []permissionauthoringv1.RecordTypeGrantChange
}

func permissionAuthoringPlannerRepoForTest() *permissionAuthoringPlannerRepo {
	return &permissionAuthoringPlannerRepo{}
}

func (r *permissionAuthoringPlannerRepo) GetPermissionSetGrants(context.Context, string) (map[string]bool, error) {
	return map[string]bool{"ManageUsers": false}, nil
}

func (r *permissionAuthoringPlannerRepo) ListSystemPermissions(context.Context) ([]models.SystemPermission, error) {
	return []models.SystemPermission{{APIName: "ManageUsers", IsActive: true}}, nil
}

func (r *permissionAuthoringPlannerRepo) SetSystemPermission(_ context.Context, _ string, apiName string, _ bool) error {
	r.writes = append(r.writes, "system:"+apiName)
	return nil
}

func (r *permissionAuthoringPlannerRepo) ListRecordTypeCatalog(context.Context) ([]ports.RecordTypeCatalogEntry, error) {
	if r.recordTypes != nil {
		return append([]ports.RecordTypeCatalogEntry(nil), r.recordTypes...), nil
	}
	return []ports.RecordTypeCatalogEntry{{RecordTypeID: "record-type-1", ObjectAPIName: "Account", Name: "record-type-1", Label: "record-type-1", IsActive: true}}, nil
}

func (r *permissionAuthoringPlannerRepo) GetPermissionSetRecordTypeGrants(context.Context, string) ([]models.PermissionSetRecordTypeVisibility, error) {
	return append([]models.PermissionSetRecordTypeVisibility(nil), r.recordTypeRows...), nil
}

func (r *permissionAuthoringPlannerRepo) SetPermissionSetRecordTypeGrant(_ context.Context, _ string, recordTypeID string, visible bool) error {
	r.writes = append(r.writes, "record-type:"+recordTypeID)
	r.recordTypeSets = append(r.recordTypeSets, permissionauthoringv1.RecordTypeGrantChange{RecordTypeID: recordTypeID, Visible: visible})
	return nil
}

func (r *permissionAuthoringPlannerRepo) ListCustomPermissions(context.Context) ([]models.SystemCustomPermission, error) {
	return []models.SystemCustomPermission{{ID: "custom-permission-1", IsActive: true}}, nil
}

func (r *permissionAuthoringPlannerRepo) GetPermissionSetCustomPermGrants(context.Context, string) ([]models.PermissionSetCustomPermission, error) {
	return nil, nil
}

func (r *permissionAuthoringPlannerRepo) SetPermissionSetCustomPermGrant(_ context.Context, _ string, customPermissionID string) error {
	r.writes = append(r.writes, "custom:"+customPermissionID)
	return nil
}

func (r *permissionAuthoringPlannerRepo) DeletePermissionSetCustomPermGrant(_ context.Context, _ string, customPermissionID string) error {
	r.writes = append(r.writes, "custom-delete:"+customPermissionID)
	return nil
}

type permissionAuthoringRecordsPlannerForTest struct {
	writes  *[]string
	planErr error
}

func (planner *permissionAuthoringRecordsPlannerForTest) PlanPermissionAuthoringRecordMutation(
	_ context.Context,
	_ string,
	objects []permissionauthoringv1.ObjectGrantChange,
	fields []permissionauthoringv1.FieldGrantChange,
	_ *models.UserSession,
) (ports.PermissionAuthoringRecordMutationPlan, error) {
	if planner.planErr != nil {
		return nil, planner.planErr
	}
	plan := &permissionAuthoringRecordsPlanForTest{writes: planner.writes}
	for _, change := range objects {
		if change.Read || change.Create || change.Edit || change.Delete || change.ViewAll || change.ModifyAll {
			plan.names = append(plan.names, "object:"+change.ResourceType)
		}
	}
	for _, change := range fields {
		if change.Read || change.Edit {
			plan.names = append(plan.names, "field:"+change.ResourceType+"."+change.FieldName)
		}
	}
	return plan, nil
}

type permissionAuthoringRecordsPlanForTest struct {
	writes *[]string
	names  []string
}

func (plan *permissionAuthoringRecordsPlanForTest) Changed() bool { return len(plan.names) != 0 }

func (plan *permissionAuthoringRecordsPlanForTest) FinalChanges() permissionauthoringv1.Changes {
	var changes permissionauthoringv1.Changes
	for _, name := range plan.names {
		if strings.HasPrefix(name, "object:") {
			changes.Objects = append(changes.Objects, permissionauthoringv1.ObjectGrantChange{
				ResourceType: strings.TrimPrefix(name, "object:"), Read: true,
			})
		}
		if strings.HasPrefix(name, "field:") {
			identity := strings.TrimPrefix(name, "field:")
			parts := strings.SplitN(identity, ".", 2)
			changes.Fields = append(changes.Fields, permissionauthoringv1.FieldGrantChange{
				ResourceType: parts[0], FieldName: parts[1], Read: true,
			})
		}
	}
	return changes
}

func (plan *permissionAuthoringRecordsPlanForTest) Apply(context.Context) error {
	*plan.writes = append(*plan.writes, plan.names...)
	return nil
}
