package packs

import (
	"context"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// stubMetadataParent is a test double for metadataParentLookup. It returns
// `parents[schema.APIName]` so callers can mark specific schemas as
// parent-controlled (non-empty parent APIName) or not.
type stubMetadataParent struct {
	parents map[string]string
}

func (s *stubMetadataParent) GetParentControlledObject(_ context.Context, schema *models.ObjectMetadata) string {
	if s == nil || s.parents == nil || schema == nil {
		return ""
	}
	return s.parents[schema.APIName]
}

// TestBuildBusinessPermissionSeedScopeSplitsObjectAndFieldCoverage verifies
// the OP/FP scope split when one of the schemas is parent-controlled:
//   - objectPermAPINames excludes the parent-controlled child
//   - fieldPermAPINames includes the parent-controlled child (child FLS
//     remains materialized per AC3)
//   - the derived-AP schema is excluded from BOTH scopes
//   - expectedObjectRowsPerProfile counts only the OP-scope schemas
//   - expectedFieldRowsPerProfile counts materializable fields across the
//     FP-scope schemas (parent-controlled included)
//   - parentControlled map records the classification for the reconcile loop
func TestBuildBusinessPermissionSeedScopeSplitsObjectAndFieldCoverage(t *testing.T) {
	derived := &models.ObjectAccessPolicy{Mode: "derived"}
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Fields: []models.FieldMetadata{
			{APIName: "Name"}, {APIName: "Description"}, {APIName: "Phone"},
		}},
		{APIName: "OpportunityLineItem", Fields: []models.FieldMetadata{
			{APIName: "Quantity"}, {APIName: "UnitPrice"},
		}},
		{APIName: "DerivedThing", AccessPolicy: derived, Fields: []models.FieldMetadata{
			{APIName: "X"},
		}},
	}

	scope := buildBusinessPermissionSeedScope(
		context.Background(),
		schemas,
		&stubMetadataParent{parents: map[string]string{
			"OpportunityLineItem": "Opportunity",
		}},
	)

	if scope.expectedObjectRowsPerProfile != 1 {
		t.Fatalf("expectedObjectRowsPerProfile = %d; want 1 (Account only)", scope.expectedObjectRowsPerProfile)
	}
	if scope.expectedFieldRowsPerProfile != 5 {
		t.Fatalf("expectedFieldRowsPerProfile = %d; want 5 (Account 3 + OLI 2)", scope.expectedFieldRowsPerProfile)
	}
	if !equalSlices(scope.objectPermAPINames, []string{"Account"}) {
		t.Fatalf("objectPermAPINames = %v; want [Account]", scope.objectPermAPINames)
	}
	if !equalSlices(scope.fieldPermAPINames, []string{"Account", "OpportunityLineItem"}) {
		t.Fatalf("fieldPermAPINames = %v; want [Account OpportunityLineItem]", scope.fieldPermAPINames)
	}
	if !scope.parentControlled["OpportunityLineItem"] {
		t.Fatal("parentControlled[OpportunityLineItem] must be true")
	}
	if scope.parentControlled["Account"] {
		t.Fatal("parentControlled[Account] must be false")
	}
}

func equalSlices(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func TestBuildBusinessPermissionSeedScopeCountsAllBusinessFields(t *testing.T) {
	scope := buildBusinessPermissionSeedScope(context.Background(), []*models.ObjectMetadata{
		{
			APIName: "Account",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Createable: false, Updateable: false},
				{APIName: "Description", Createable: true, Updateable: true},
				{APIName: "LastActivityDate", Createable: false, Updateable: false},
			},
		},
	}, &stubMetadataParent{})

	if scope.expectedObjectRowsPerProfile != 1 {
		t.Fatalf("expectedObjectRowsPerProfile = %d; want 1", scope.expectedObjectRowsPerProfile)
	}
	if scope.expectedFieldRowsPerProfile != 3 {
		t.Fatalf("expectedFieldRowsPerProfile = %d; want 3", scope.expectedFieldRowsPerProfile)
	}
	if len(scope.objectPermAPINames) != 1 || scope.objectPermAPINames[0] != "Account" {
		t.Fatalf("objectPermAPINames = %#v; want [Account]", scope.objectPermAPINames)
	}
	if len(scope.fieldPermAPINames) != 1 || scope.fieldPermAPINames[0] != "Account" {
		t.Fatalf("fieldPermAPINames = %#v; want [Account]", scope.fieldPermAPINames)
	}
}

func TestReconcileBusinessPermissionCoverageFiltersProfilesBeforeCounting(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("FROM .*UserRole.*").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Name", "RollupDescription", "ParentRoleId"}))

	objectCountQuery := "SELECT ps\\.`ProfileId`, COUNT\\(\\*\\).*ps\\.`ProfileId` IN \\(\\?,\\?\\).*op\\.`SobjectType` IN \\(\\?\\)"
	mock.ExpectQuery(objectCountQuery).
		WithArgs(constants.ProfileSystemAdmin, constants.ProfileStandardUser, "Account").
		WillReturnRows(sqlmock.NewRows([]string{"ProfileId", "count"}).
			AddRow(constants.ProfileSystemAdmin, 1).
			AddRow(constants.ProfileStandardUser, 1))

	fieldCountQuery := "SELECT ps\\.`ProfileId`, COUNT\\(\\*\\).*ps\\.`ProfileId` IN \\(\\?,\\?\\).*fp\\.`SobjectType` IN \\(\\?\\)"
	mock.ExpectQuery(fieldCountQuery).
		WithArgs(constants.ProfileSystemAdmin, constants.ProfileStandardUser, "Account").
		WillReturnRows(sqlmock.NewRows([]string{"ProfileId", "count"}).
			AddRow(constants.ProfileSystemAdmin, 1).
			AddRow(constants.ProfileStandardUser, 1))

	permSvc := permissions.NewPermissionService(persistence.NewPermissionRepository(db), nil, nil)
	scope := permissionSeedScope{
		schemas:                      []*models.ObjectMetadata{{APIName: "Account"}},
		objectPermAPINames:           []string{"Account"},
		fieldPermAPINames:            []string{"Account"},
		expectedObjectRowsPerProfile: 1,
		expectedFieldRowsPerProfile:  1,
	}
	profileInfos := []struct{ ID, Name string }{
		{ID: constants.ProfileSystemAdmin, Name: "System Administrator"},
		{ID: "custom_profile", Name: "Custom Profile"},
		{ID: constants.ProfileStandardUser, Name: "Standard User"},
	}

	if err := reconcileBusinessPermissionCoverage(context.Background(), permSvc, profileInfos, nil, scope); err != nil {
		t.Fatalf("reconcileBusinessPermissionCoverage: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
