package permissions

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// accessDecisionGoldenPath is the checked-in regression snapshot. Regenerate with
// UPDATE_GOLDEN=1; otherwise the test asserts the live read paths still produce it.
const accessDecisionGoldenPath = "testdata/access_decision_golden.json"

// TestAccessDecisionGolden is a Phase-2 L0-A capture-and-freeze regression: it
// drives ONLY existing read paths on *PermissionService (object CRUD, field
// visibility/editability, record-row access, RLS shape, and the contract admin
// gate) over toy objects/users, then asserts the resulting access-decision matrix
// byte-for-byte against testdata/access_decision_golden.json. It exists so the
// upcoming L0-D migration cannot silently change a single decision. It adds no
// evaluator and touches no production code; coherence is achieved purely by the
// fake repo/metadata data below.
//
// Regenerate:  UPDATE_GOLDEN=1 GOWORK=off go test ./... -run AccessDecisionGolden
func TestAccessDecisionGolden(t *testing.T) {
	ctx := context.Background()

	// --- Toy metadata ---------------------------------------------------------
	// Widget: PublicReadWrite OWD  -> every user has row access regardless of owner.
	// PrivateDoc: Private OWD       -> only the owner / a share / a team grant gives row access.
	widget := &models.ObjectMetadata{
		APIName:      "Widget",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, IsSystem: true},
			{APIName: constants.FieldOwnerID},
			{APIName: "Name", Createable: true, Updateable: true},
			{APIName: "Locked__c", Createable: true, Updateable: true},
		},
	}
	privateDoc := &models.ObjectMetadata{
		APIName:      "PrivateDoc",
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, IsSystem: true},
			{APIName: constants.FieldOwnerID},
			{APIName: "Name", Createable: true, Updateable: true},
			{APIName: "Locked__c", Createable: true, Updateable: true},
		},
	}
	metadata := &accessDecisionGoldenMetadata{schemas: []*models.ObjectMetadata{widget, privateDoc}}

	// --- Toy users ------------------------------------------------------------
	owner := &models.UserSession{ID: "user-owner", ProfileID: "profile-standard"}
	nonOwner := &models.UserSession{ID: "user-nonowner", ProfileID: "profile-standard"}
	admin := &models.UserSession{ID: "user-admin", ProfileID: "profile-admin"}

	// --- Toy coherent fake data ----------------------------------------------
	// Object CRUD is driven by the effective ObjectPermissions row (profile/permset
	// plane), independent of the row-level OWD. We give the owner/non-owner Read+Edit
	// but NOT Create/Delete so the CRUD column is meaningful (mixed true/false).
	objCRUD := func(obj string) *models.ObjectPermissions {
		return &models.ObjectPermissions{
			SobjectType:       obj,
			PermissionsRead:   true,
			PermissionsCreate: false,
			PermissionsEdit:   true,
			PermissionsDelete: false,
		}
	}
	repo := &accessDecisionGoldenRepo{
		objectPerms: map[string]*models.ObjectPermissions{
			accessDecisionGoldenKey(owner.ID, "Widget"):        objCRUD("Widget"),
			accessDecisionGoldenKey(owner.ID, "PrivateDoc"):    objCRUD("PrivateDoc"),
			accessDecisionGoldenKey(nonOwner.ID, "Widget"):     objCRUD("Widget"),
			accessDecisionGoldenKey(nonOwner.ID, "PrivateDoc"): objCRUD("PrivateDoc"),
		},
		fieldPerms: map[string]*models.FieldPermissions{
			// Name: readable + editable.
			accessDecisionGoldenKey(owner.ID, "PrivateDoc", "Name"): {
				SobjectType: "PrivateDoc", Field: "PrivateDoc.Name",
				PermissionsRead: true, PermissionsEdit: true,
			},
			// Locked__c: readable but NOT editable (a read-only field).
			accessDecisionGoldenKey(owner.ID, "PrivateDoc", "Locked__c"): {
				SobjectType: "PrivateDoc", Field: "PrivateDoc.Locked__c",
				PermissionsRead: true, PermissionsEdit: false,
			},
		},
		// Admin holds ViewSetup -> passes the EffectivePermissions admin gate.
		// Owner/non-owner hold nothing -> denied when viewing another user.
		systemPerms: map[string]map[string]bool{
			admin.ID: {string(constants.PermViewSetup): true},
		},
		userProfiles: map[string]string{
			owner.ID: "profile-standard",
		},
		// PrivateDoc rows.
		manualShares: map[string][]string{
			// non-owner is granted Read on the "shared" record.
			accessDecisionGoldenKey("PrivateDoc", "doc-shared", nonOwner.ID): {constants.PermRead},
		},
		teamAccess: map[string]string{
			// non-owner has Edit team access on the "team" record (Edit implies Read).
			accessDecisionGoldenKey("PrivateDoc", "doc-team", nonOwner.ID): constants.PermEdit,
		},
	}

	svc := NewPermissionService(repo, metadata, nil)
	admin0 := NewContractPermissionAdmin(svc, nil)

	// --- Build the matrix by calling ONLY existing read paths -----------------
	matrix := accessDecisionMatrix{}
	add := func(surface, principal, target, operation string, allow bool) {
		matrix.Decisions = append(matrix.Decisions, accessDecisionRow{
			Surface: surface, Principal: principal, Target: target, Operation: operation, Allow: allow,
		})
	}

	// (1) Object CRUD: Widget (Public) and PrivateDoc (Private) x {Read,Create,Edit,Delete}.
	for _, obj := range []string{"Widget", "PrivateDoc"} {
		for _, op := range []string{constants.PermRead, constants.PermCreate, constants.PermEdit, constants.PermDelete} {
			add("object", "owner", obj, op, svc.CheckObjectPermissionWithUser(ctx, obj, op, owner))
		}
	}

	// (2) Field visibility + editability: a readable+editable field and a read-only field.
	for _, field := range []string{"Name", "Locked__c"} {
		add("field", "owner", "PrivateDoc."+field, "visible",
			mustCheckFieldVisibilityWithUser(t, svc, ctx, "PrivateDoc", field, owner))
		add("field", "owner", "PrivateDoc."+field, "editable",
			mustCheckFieldEditabilityWithUser(t, svc, ctx, "PrivateDoc", field, owner))
	}

	// (3) Record-row access via CheckRecordAccess.
	//   PrivateDoc: owner / non-owner no-share / non-owner manual-share / non-owner team.
	//   Widget (Public OWD): non-owner still gets access.
	docOwned := models.SObject{constants.FieldID: "doc-owned", constants.FieldOwnerID: owner.ID}
	docNoShare := models.SObject{constants.FieldID: "doc-noshare", constants.FieldOwnerID: owner.ID}
	docShared := models.SObject{constants.FieldID: "doc-shared", constants.FieldOwnerID: owner.ID}
	docTeam := models.SObject{constants.FieldID: "doc-team", constants.FieldOwnerID: owner.ID}
	widgetRec := models.SObject{constants.FieldID: "widget-1", constants.FieldOwnerID: owner.ID}

	add("row", "owner", "PrivateDoc/doc-owned", "read",
		svc.CheckRecordAccess(ctx, privateDoc, docOwned, constants.PermRead, owner))
	add("row", "nonowner", "PrivateDoc/doc-noshare", "read",
		svc.CheckRecordAccess(ctx, privateDoc, docNoShare, constants.PermRead, nonOwner))
	add("row", "nonowner", "PrivateDoc/doc-shared", "read",
		svc.CheckRecordAccess(ctx, privateDoc, docShared, constants.PermRead, nonOwner))
	add("row", "nonowner", "PrivateDoc/doc-team", "read",
		svc.CheckRecordAccess(ctx, privateDoc, docTeam, constants.PermRead, nonOwner))
	add("row", "nonowner", "PrivateDoc/doc-team", "edit",
		svc.CheckRecordAccess(ctx, privateDoc, docTeam, constants.PermEdit, nonOwner))
	add("row", "nonowner", "Widget/widget-1", "read",
		svc.CheckRecordAccess(ctx, widget, widgetRec, constants.PermRead, nonOwner))

	// (4) RLS result shape (stable, serializable fields only — no pointers).
	for _, obj := range []struct {
		name   string
		schema *models.ObjectMetadata
	}{
		{"PrivateDoc", privateDoc},
		{"Widget", widget},
	} {
		rls := svc.BuildRLSResult(ctx, obj.schema, obj.name, owner)
		matrix.RLS = append(matrix.RLS, accessDecisionRLS{
			Object:              rls.ObjectAPIName,
			Skip:                rls.Skip,
			DenyAll:             rls.DenyAll,
			HasOwnerField:       rls.HasOwnerField,
			OwnerUserIDs:        sortedCopy(rls.OwnerUserIDs),
			IncludeManualShares: rls.IncludeManualShares,
			IncludeTeamMembers:  rls.IncludeTeamMembers,
		})
	}

	// (5) Contract admin gate: viewer WITHOUT ViewSetup viewing another user (forbidden)
	//     vs viewer WITH ViewSetup (admin) viewing another user (allowed).
	_, errNoGate := admin0.EffectivePermissions(ctx, nonOwner, owner.ID)
	matrix.Admin = append(matrix.Admin, accessDecisionAdmin{
		Scenario: "non_admin_views_other_user",
		Allowed:  errNoGate == nil,
		Error:    permissionAdminGoldenErrorKind(errNoGate),
	})
	_, errGate := admin0.EffectivePermissions(ctx, admin, owner.ID)
	matrix.Admin = append(matrix.Admin, accessDecisionAdmin{
		Scenario: "admin_views_other_user",
		Allowed:  errGate == nil,
		Error:    permissionAdminGoldenErrorKind(errGate),
	})

	// --- Golden compare / regenerate -----------------------------------------
	got, err := json.MarshalIndent(matrix, "", "  ")
	if err != nil {
		t.Fatalf("marshal access decision matrix: %v", err)
	}
	got = append(got, '\n')

	if os.Getenv("UPDATE_GOLDEN") == "1" {
		if err := os.MkdirAll(filepath.Dir(accessDecisionGoldenPath), 0o755); err != nil {
			t.Fatalf("mkdir testdata: %v", err)
		}
		if err := os.WriteFile(accessDecisionGoldenPath, got, 0o644); err != nil {
			t.Fatalf("write golden: %v", err)
		}
		t.Logf("regenerated %s (%d decisions)", accessDecisionGoldenPath, len(matrix.Decisions))
		return
	}

	want, err := os.ReadFile(accessDecisionGoldenPath)
	if err != nil {
		t.Fatalf("read golden %s (run with UPDATE_GOLDEN=1 to create): %v", accessDecisionGoldenPath, err)
	}
	if string(got) != string(want) {
		t.Fatalf("access decision golden drifted.\n--- got ---\n%s\n--- want ---\n%s\n"+
			"If this change is intentional, regenerate with UPDATE_GOLDEN=1.", got, want)
	}
}

// --- Golden matrix DTOs (stable JSON shape) ----------------------------------

type accessDecisionMatrix struct {
	Decisions []accessDecisionRow   `json:"decisions"`
	RLS       []accessDecisionRLS   `json:"rls"`
	Admin     []accessDecisionAdmin `json:"admin"`
}

type accessDecisionRow struct {
	Surface   string `json:"surface"`
	Principal string `json:"principal"`
	Target    string `json:"target"`
	Operation string `json:"operation"`
	Allow     bool   `json:"allow"`
}

type accessDecisionRLS struct {
	Object              string   `json:"object"`
	Skip                bool     `json:"skip"`
	DenyAll             bool     `json:"denyAll"`
	HasOwnerField       bool     `json:"hasOwnerField"`
	OwnerUserIDs        []string `json:"ownerUserIds,omitempty"`
	IncludeManualShares bool     `json:"includeManualShares"`
	IncludeTeamMembers  bool     `json:"includeTeamMembers"`
}

type accessDecisionAdmin struct {
	Scenario string `json:"scenario"`
	Allowed  bool   `json:"allowed"`
	Error    string `json:"error,omitempty"`
}

func sortedCopy(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	out := append([]string(nil), in...)
	sort.Strings(out)
	return out
}

func permissionAdminGoldenErrorKind(err error) string {
	if err == nil {
		return ""
	}
	var adminErr *contract.PermissionAdminError
	if errors.As(err, &adminErr) {
		return string(adminErr.Kind)
	}
	return "unknown"
}

// --- Fakes (reuse saveTestRepo base; override only the read paths used) -------

type accessDecisionGoldenMetadata struct {
	schemas        []*models.ObjectMetadata
	recordTypes    []*models.RecordType
	recordTypesErr error
}

func (m *accessDecisionGoldenMetadata) GetSchema(_ context.Context, apiName string) *models.ObjectMetadata {
	for _, schema := range m.schemas {
		if strings.EqualFold(schema.APIName, apiName) {
			return schema
		}
	}
	return nil
}

func (m *accessDecisionGoldenMetadata) GetSchemas(context.Context) []*models.ObjectMetadata {
	return append([]*models.ObjectMetadata(nil), m.schemas...)
}

func (*accessDecisionGoldenMetadata) GetRelationships(context.Context, string) []*models.Relationship {
	return nil
}

func (*accessDecisionGoldenMetadata) GetSharingParentRelationship(context.Context, string) *models.Relationship {
	return nil
}

func (*accessDecisionGoldenMetadata) GetParentControlledObject(context.Context, *models.ObjectMetadata) string {
	return ""
}

func (*accessDecisionGoldenMetadata) GetSharingRules(context.Context, string) []*models.SharingRule {
	return nil
}

func (m *accessDecisionGoldenMetadata) GetRecordTypes(context.Context, string) ([]*models.RecordType, error) {
	return append([]*models.RecordType(nil), m.recordTypes...), m.recordTypesErr
}

func TestEffectiveAccessFailsClosedWhenRecordTypeAuthorityIsUnavailable(t *testing.T) {
	want := errors.New("record-type authority unavailable")
	metadata := &accessDecisionGoldenMetadata{
		schemas:        []*models.ObjectMetadata{{APIName: "Widget"}},
		recordTypesErr: want,
	}
	service := NewPermissionService(&accessDecisionGoldenRepo{}, metadata, nil)
	if _, err := service.EffectiveAccess(t.Context(), &models.UserSession{ID: "user-1"}, "Widget"); !errors.Is(err, want) {
		t.Fatalf("EffectiveAccess record-type error = %v, want %v", err, want)
	}
}

type accessDecisionGoldenRepo struct {
	saveTestRepo

	objectPerms  map[string]*models.ObjectPermissions
	fieldPerms   map[string]*models.FieldPermissions
	systemPerms  map[string]map[string]bool
	customPerms  map[string]map[string]bool
	userProfiles map[string]string
	manualShares map[string][]string
	teamAccess   map[string]string
}

func (r *accessDecisionGoldenRepo) LoadEffectiveObjectPermission(_ context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
	if user == nil {
		return nil, nil
	}
	if perm := r.objectPerms[accessDecisionGoldenKey(user.ID, objectAPIName)]; perm != nil {
		cp := *perm
		return &cp, nil
	}
	return nil, nil
}

func (r *accessDecisionGoldenRepo) LoadEffectiveFieldPermission(_ context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
	if user == nil {
		return nil, nil
	}
	if perm := r.fieldPerms[accessDecisionGoldenKey(user.ID, objectAPIName, fieldAPIName)]; perm != nil {
		cp := *perm
		return &cp, nil
	}
	return nil, nil
}

func (r *accessDecisionGoldenRepo) GetEffectiveSystemPermissions(_ context.Context, userID, _ string) (map[string]bool, error) {
	return copyBoolMap(r.systemPerms[userID]), nil
}

func (r *accessDecisionGoldenRepo) GetEffectiveCustomPermissions(_ context.Context, userID, _ string) (map[string]bool, error) {
	return copyBoolMap(r.customPerms[userID]), nil
}

func (r *accessDecisionGoldenRepo) GetUserProfileID(_ context.Context, userID string) (string, error) {
	return r.userProfiles[userID], nil
}

func (r *accessDecisionGoldenRepo) ListEffectiveObjectPermissionsForUser(_ context.Context, userID, _ string) ([]models.ObjectPermissions, error) {
	var out []models.ObjectPermissions
	for key, perm := range r.objectPerms {
		if !strings.HasPrefix(key, userID+"\x00") || perm == nil {
			continue
		}
		out = append(out, *perm)
	}
	return out, nil
}

func (r *accessDecisionGoldenRepo) ListEffectiveFieldPermissionsForUser(_ context.Context, userID, _ string) ([]models.FieldPermissions, error) {
	var out []models.FieldPermissions
	for key, perm := range r.fieldPerms {
		if !strings.HasPrefix(key, userID+"\x00") || perm == nil {
			continue
		}
		out = append(out, *perm)
	}
	return out, nil
}

func (r *accessDecisionGoldenRepo) GetManualShareAccessLevels(_ context.Context, objectAPIName, recordID, userID string) ([]string, error) {
	return append([]string(nil), r.manualShares[accessDecisionGoldenKey(objectAPIName, recordID, userID)]...), nil
}

func (r *accessDecisionGoldenRepo) GetTeamMemberAccessLevel(_ context.Context, objectAPIName, recordID, userID string) (*string, error) {
	level := r.teamAccess[accessDecisionGoldenKey(objectAPIName, recordID, userID)]
	if level == "" {
		return nil, nil
	}
	return &level, nil
}

func (r *accessDecisionGoldenRepo) GetManualShareRecordIDs(_ context.Context, objectAPIName, userID string) ([]string, error) {
	var out []string
	prefix := accessDecisionGoldenKey(objectAPIName)
	for key := range r.manualShares {
		parts := strings.Split(key, "\x00")
		if len(parts) == 3 && strings.HasPrefix(key, prefix+"\x00") && parts[2] == userID {
			out = append(out, parts[1])
		}
	}
	return out, nil
}

func (r *accessDecisionGoldenRepo) GetTeamMemberRecordIDs(_ context.Context, objectAPIName, userID string) ([]string, error) {
	var out []string
	prefix := accessDecisionGoldenKey(objectAPIName)
	for key := range r.teamAccess {
		parts := strings.Split(key, "\x00")
		if len(parts) == 3 && strings.HasPrefix(key, prefix+"\x00") && parts[2] == userID {
			out = append(out, parts[1])
		}
	}
	return out, nil
}

func (*accessDecisionGoldenRepo) IsUserInGroup(context.Context, string, string) (bool, error) {
	return false, nil
}

func (*accessDecisionGoldenRepo) GetUserGroupIDs(context.Context, string) ([]string, error) {
	return nil, nil
}

func (*accessDecisionGoldenRepo) GetGroupMemberIDs(context.Context, []string) ([]string, error) {
	return nil, nil
}

func (*accessDecisionGoldenRepo) GetAllRoles(context.Context) ([]*models.UserRole, error) {
	return nil, nil
}

func accessDecisionGoldenKey(parts ...string) string {
	return strings.Join(parts, "\x00")
}

func copyBoolMap(in map[string]bool) map[string]bool {
	out := make(map[string]bool, len(in))
	for key, value := range in {
		out[key] = value
	}
	return out
}

var _ ports.PermissionRepository = (*accessDecisionGoldenRepo)(nil)
