//go:build crm

package crm

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

	"github.com/nexuscrm/permissions"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

func TestAssignmentPolicyEligibilityProjectionDeclaresExactReadInventory(t *testing.T) {
	provider := newAssignmentPolicyEligibilityProjectionProvider()
	want := []cp.ComponentReadSurface{
		{Table: "Profile", Fact: "CRM assignment-policy Profile.Name selector resolution"},
		{Table: "User", Fact: "CRM assignment-policy active User eligibility population"},
	}
	if got := provider.AssignmentPolicyEligibilityReadSurfaces(); !reflect.DeepEqual(got, want) {
		t.Fatalf("read surfaces = %#v, want %#v", got, want)
	}

	_, lifecycle, err := permissions.NewPermissionSetAssignmentPolicyManagedLifecycleRegistrationWithEligibility(provider)
	if err != nil {
		t.Fatalf("NewPermissionSetAssignmentPolicyManagedLifecycleRegistrationWithEligibility: %v", err)
	}
	if lifecycle.Budget().MaxBatchRows < assignmentPolicyEligibilityProjectionPageRows {
		t.Fatalf(
			"managed lifecycle max batch rows = %d, adapter page rows = %d",
			lifecycle.Budget().MaxBatchRows,
			assignmentPolicyEligibilityProjectionPageRows,
		)
	}
	readFacts := map[string]string{}
	for _, surface := range lifecycle.ReadSet() {
		readFacts[surface.Table] = surface.Fact
	}
	for _, surface := range want {
		if got := readFacts[surface.Table]; got != surface.Fact {
			t.Fatalf("registration read fact for %s = %q, want %q", surface.Table, got, surface.Fact)
		}
	}
}

func TestAssignmentPolicyEligibilityProjectionExhaustsUserKeysetPages(t *testing.T) {
	profileIDs := map[string]string{
		"Standard User": "profile-standard",
		"Sales User":    "profile-sales",
	}
	store := &assignmentPolicyEligibilityStoreStub{
		readRows: func(_ context.Context, query cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
			selector, ok := query.Equal["Name"].(string)
			if query.Table != "Profile" || !ok || query.MaximumRows != 2 ||
				!reflect.DeepEqual(query.Columns, []string{"Id", "IsActive", "IsDeleted"}) {
				t.Fatalf("Profile query = %#v", query)
			}
			return []cp.ComponentRow{{
				"Id":        profileIDs[selector],
				"IsActive":  []byte("1"),
				"IsDeleted": "false",
			}}, nil
		},
		readRowsKeysetPage: func(_ context.Context, query cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
			if query.Query.Table != "User" ||
				!reflect.DeepEqual(query.Query.Columns, []string{"Id"}) ||
				!reflect.DeepEqual(query.Query.LogicalKeyColumns, []string{"Id"}) ||
				!reflect.DeepEqual(query.Query.OrderBy, []cp.ComponentManagedOrder{{Column: "Id", Direction: cp.ComponentManagedOrderAscending}}) ||
				query.Query.MaximumRows != assignmentPolicyEligibilityProjectionPageRows ||
				query.Query.Equal["IsActive"] != true || query.Query.Equal["IsDeleted"] != false {
				t.Fatalf("User query = %#v", query)
			}
			profileID := query.Query.Equal["ProfileId"]
			switch profileID {
			case "profile-standard":
				if len(query.ContinueAfter) == 0 {
					return cp.ComponentManagedKeysetRowPage{
						Rows: []cp.ComponentRow{{"Id": []byte("user-1")}},
					}, nil
				}
				if !reflect.DeepEqual(query.ContinueAfter, cp.ComponentRow{"Id": "user-1"}) {
					t.Fatalf("standard User continuation = %#v", query.ContinueAfter)
				}
				return cp.ComponentManagedKeysetRowPage{
					Rows:      []cp.ComponentRow{{"Id": "user-2"}},
					Exhausted: true,
				}, nil
			case "profile-sales":
				return cp.ComponentManagedKeysetRowPage{
					Rows:      []cp.ComponentRow{{"Id": "user-3"}},
					Exhausted: true,
				}, nil
			default:
				t.Fatalf("User ProfileId = %#v", profileID)
				return cp.ComponentManagedKeysetRowPage{}, nil
			}
		},
	}

	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User", "Sales User"},
	)
	if err != nil {
		t.Fatalf("ProjectAssignmentPolicyEligibility: %v", err)
	}
	wantSubjects := []string{"user-1", "user-2", "user-3"}
	if projection.Resolution != permissions.AssignmentPolicyEligibilityResolved || !reflect.DeepEqual(projection.SubjectIDs, wantSubjects) {
		t.Fatalf("projection = %#v, want resolved subjects %#v", projection, wantSubjects)
	}
}

func TestAssignmentPolicyEligibilityProjectionClassifiesInvalidProfileSelectors(t *testing.T) {
	tests := []struct {
		name string
		rows []cp.ComponentRow
	}{
		{name: "missing"},
		{name: "ambiguous", rows: []cp.ComponentRow{
			{"Id": "profile-a", "IsActive": int64(1), "IsDeleted": int64(0)},
			{"Id": "profile-b", "IsActive": int64(1), "IsDeleted": int64(0)},
		}},
		{name: "inactive", rows: []cp.ComponentRow{
			{"Id": "profile-a", "IsActive": int64(0), "IsDeleted": int64(0)},
		}},
		{name: "deleted", rows: []cp.ComponentRow{
			{"Id": "profile-a", "IsActive": int64(1), "IsDeleted": int64(1)},
		}},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			userReads := 0
			store := &assignmentPolicyEligibilityStoreStub{
				readRows: func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
					return test.rows, nil
				},
				readRowsKeysetPage: func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
					userReads++
					return cp.ComponentManagedKeysetRowPage{}, nil
				},
			}

			projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
				context.Background(), store, []string{"Standard User"},
			)
			if err != nil {
				t.Fatalf("ProjectAssignmentPolicyEligibility: %v", err)
			}
			if projection.Resolution != permissions.AssignmentPolicyEligibilityInvalidSelector || len(projection.SubjectIDs) != 0 {
				t.Fatalf("projection = %#v, want invalid selector without subjects", projection)
			}
			if userReads != 0 {
				t.Fatalf("User reads = %d, want 0 after invalid selector", userReads)
			}
		})
	}
}

func TestAssignmentPolicyEligibilityProjectionKeepsReadUncertaintyOutOfInvalidSelector(t *testing.T) {
	readErr := errors.New("database unavailable")
	store := &assignmentPolicyEligibilityStoreStub{
		readRows: func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
			return nil, readErr
		},
		readRowsKeysetPage: func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
			return cp.ComponentManagedKeysetRowPage{}, nil
		},
	}
	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if !errors.Is(err, readErr) {
		t.Fatalf("error = %v, want wrapped read error", err)
	}
	if projection.Resolution != "" || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want zero projection on uncertain read", projection)
	}

	store.readRows = func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
		return []cp.ComponentRow{{"Id": "profile-standard", "IsActive": "not-a-boolean", "IsDeleted": false}}, nil
	}
	projection, err = newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if err == nil || !strings.Contains(err.Error(), "IsActive") {
		t.Fatalf("error = %v, want malformed Profile row error", err)
	}
	if projection.Resolution != "" || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want zero projection on malformed row", projection)
	}

	userReadErr := errors.New("User scan unavailable")
	store.readRows = func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
		return []cp.ComponentRow{{"Id": "profile-standard", "IsActive": true, "IsDeleted": false}}, nil
	}
	store.readRowsKeysetPage = func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
		return cp.ComponentManagedKeysetRowPage{}, userReadErr
	}
	projection, err = newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if !errors.Is(err, userReadErr) {
		t.Fatalf("error = %v, want wrapped User read error", err)
	}
	if projection.Resolution != "" || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want zero projection on uncertain User read", projection)
	}
}

func TestAssignmentPolicyEligibilityProjectionFailsClosedWithoutExactKeysetReads(t *testing.T) {
	store := assignmentPolicyEligibilityReadStoreFunc(func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
		return []cp.ComponentRow{{
			"Id": "profile-standard", "IsActive": true, "IsDeleted": false,
		}}, nil
	})
	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if err == nil || !strings.Contains(err.Error(), "requires exact keyset reads") {
		t.Fatalf("error = %v, want exact keyset requirement", err)
	}
	if projection.Resolution != "" || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want zero projection without exact exhaustion", projection)
	}
}

func TestAssignmentPolicyEligibilityProjectionRejectsNonAdvancingUserPage(t *testing.T) {
	store := &assignmentPolicyEligibilityStoreStub{
		readRows: func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
			return []cp.ComponentRow{{
				"Id": "profile-standard", "IsActive": true, "IsDeleted": false,
			}}, nil
		},
		readRowsKeysetPage: func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
			return cp.ComponentManagedKeysetRowPage{}, nil
		},
	}
	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if err == nil || !strings.Contains(err.Error(), "did not advance") {
		t.Fatalf("error = %v, want non-advancing page error", err)
	}
	if projection.Resolution != "" || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want zero projection on non-advancing page", projection)
	}
}

func TestAssignmentPolicyEligibilityProjectionRejectsEmptySelectorSet(t *testing.T) {
	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), nil, nil,
	)
	if err != nil {
		t.Fatalf("ProjectAssignmentPolicyEligibility: %v", err)
	}
	if projection.Resolution != permissions.AssignmentPolicyEligibilityInvalidSelector || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want invalid selector without subjects", projection)
	}
}

func TestAssignmentPolicyEligibilityProjectionResolvesEmptyEligiblePopulation(t *testing.T) {
	store := &assignmentPolicyEligibilityStoreStub{
		readRows: func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
			return []cp.ComponentRow{{
				"Id": "profile-standard", "IsActive": true, "IsDeleted": false,
			}}, nil
		},
		readRowsKeysetPage: func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
			return cp.ComponentManagedKeysetRowPage{Exhausted: true}, nil
		},
	}
	projection, err := newAssignmentPolicyEligibilityProjectionProvider().ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if err != nil {
		t.Fatalf("ProjectAssignmentPolicyEligibility: %v", err)
	}
	if projection.Resolution != permissions.AssignmentPolicyEligibilityResolved || len(projection.SubjectIDs) != 0 {
		t.Fatalf("projection = %#v, want resolved empty population", projection)
	}
}

func TestAssignmentPolicyEligibilityProjectionResolvesActiveUsersForProfileName(t *testing.T) {
	provider := newAssignmentPolicyEligibilityProjectionProvider()
	store := &assignmentPolicyEligibilityStoreStub{
		readRows: func(_ context.Context, query cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error) {
			if query.Table != "Profile" {
				t.Fatalf("ReadRows table = %q, want Profile", query.Table)
			}
			return []cp.ComponentRow{{
				"Id":        "profile-standard",
				"IsActive":  true,
				"IsDeleted": false,
			}}, nil
		},
		readRowsKeysetPage: func(_ context.Context, query cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error) {
			if query.Query.Table != "User" {
				t.Fatalf("ReadRowsKeysetPage table = %q, want User", query.Query.Table)
			}
			return cp.ComponentManagedKeysetRowPage{
				Rows:      []cp.ComponentRow{{"Id": "user-1"}},
				Exhausted: true,
			}, nil
		},
	}

	projection, err := provider.ProjectAssignmentPolicyEligibility(
		context.Background(), store, []string{"Standard User"},
	)
	if err != nil {
		t.Fatalf("ProjectAssignmentPolicyEligibility: %v", err)
	}
	if projection.Resolution != permissions.AssignmentPolicyEligibilityResolved {
		t.Fatalf("resolution = %q, want %q", projection.Resolution, permissions.AssignmentPolicyEligibilityResolved)
	}
	if len(projection.SubjectIDs) != 1 || projection.SubjectIDs[0] != "user-1" {
		t.Fatalf("subject IDs = %#v, want [user-1]", projection.SubjectIDs)
	}
}

type assignmentPolicyEligibilityStoreStub struct {
	readRows           func(context.Context, cp.ComponentManagedRowQuery) ([]cp.ComponentRow, error)
	readRowsKeysetPage func(context.Context, cp.ComponentManagedKeysetRowQuery) (cp.ComponentManagedKeysetRowPage, error)
}

type assignmentPolicyEligibilityReadStoreFunc func(
	context.Context,
	cp.ComponentManagedRowQuery,
) ([]cp.ComponentRow, error)

func (f assignmentPolicyEligibilityReadStoreFunc) ReadRows(
	ctx context.Context,
	query cp.ComponentManagedRowQuery,
) ([]cp.ComponentRow, error) {
	return f(ctx, query)
}

func (s *assignmentPolicyEligibilityStoreStub) ReadRows(
	ctx context.Context,
	query cp.ComponentManagedRowQuery,
) ([]cp.ComponentRow, error) {
	return s.readRows(ctx, query)
}

func (s *assignmentPolicyEligibilityStoreStub) ReadRowsPage(
	ctx context.Context,
	query cp.ComponentManagedKeysetRowQuery,
) ([]cp.ComponentRow, error) {
	page, err := s.readRowsKeysetPage(ctx, query)
	return page.Rows, err
}

func (s *assignmentPolicyEligibilityStoreStub) ReadRowsKeysetPage(
	ctx context.Context,
	query cp.ComponentManagedKeysetRowQuery,
) (cp.ComponentManagedKeysetRowPage, error) {
	return s.readRowsKeysetPage(ctx, query)
}
