package records

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ownerResolutionErrorRepo wraps stubUpsertRecordRepo so a *PersistenceService
// can be built whose FindOne always errors, exercising the resolver's
// fail-closed error propagation.
type ownerResolutionErrorRepo struct {
	stubUpsertRecordRepo
	err error
}

func (r *ownerResolutionErrorRepo) FindOne(_ context.Context, _ string, _ string) (models.SObject, error) {
	return nil, r.err
}

// systemManagedOwnerSpec builds a one-path spec from the given hops; keeps the
// engine tests readable without repeating models plumbing.
func systemManagedOwnerSpec(hops ...models.OwnerResolutionHop) *models.OwnerResolutionSpec {
	return &models.OwnerResolutionSpec{Paths: []models.OwnerResolutionPath{{Hops: hops}}}
}

func TestResolveSystemManagedOwnerSingleHopProfileFamily(t *testing.T) {
	ps, _ := newPermissionResourceLockService(permissionResourceLockRows())
	spec := systemProfileOwnedImmutableOwnerResolutionSpec()
	ctx := context.Background()

	cases := []struct {
		name   string
		record models.SObject
		owned  bool
	}{
		{"system profile", models.SObject{"ProfileId": "profile-system"}, true},
		{"custom profile", models.SObject{"ProfileId": "profile-custom"}, false},
		{"empty fk", models.SObject{"ProfileId": ""}, false},
		{"missing fk", models.SObject{}, false},
		{"orphan fk", models.SObject{"ProfileId": "does-not-exist"}, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			owned, err := ps.resolveSystemManagedOwner(ctx, spec, tc.record)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if owned != tc.owned {
				t.Fatalf("owned = %v, want %v", owned, tc.owned)
			}
		})
	}
}

func TestResolveSystemManagedOwnerStorageShapedSystemFlag(t *testing.T) {
	// isGuardFieldTruthy treats "1"/1/true as truthy; the resolver must honor
	// storage-shaped IsSystem values exactly like the legacy isSystemProfileID.
	ps, _ := newPermissionResourceLockService(map[string]models.SObject{
		strings.ToLower(constants.TableProfile) + ":profile-storage": {
			constants.FieldID:               "profile-storage",
			constants.FieldProfile_IsSystem: 1,
		},
	})
	spec := systemProfileOwnedImmutableOwnerResolutionSpec()

	owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, models.SObject{"ProfileId": "profile-storage"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !owned {
		t.Fatal("expected storage-shaped IsSystem=1 to resolve as system-managed")
	}
}

func TestResolveSystemManagedOwnerGateShortCircuitsPath(t *testing.T) {
	// Two-hop shape: PermissionSetId -> PermissionSet[IsOwnedByProfile gate]
	// -> ProfileId -> Profile.IsSystem. A failed gate must make the path not
	// match, mirroring isSystemProfileOwnedPermissionSetRecord.
	ps, _ := newPermissionResourceLockService(map[string]models.SObject{
		strings.ToLower(constants.TablePermissionSet) + ":ps-not-owned": {
			constants.FieldID:                             "ps-not-owned",
			constants.FieldPermissionSet_ProfileID:        "profile-system",
			constants.FieldPermissionSet_IsOwnedByProfile: false,
		},
		strings.ToLower(constants.TablePermissionSet) + ":ps-owned-system": {
			constants.FieldID:                             "ps-owned-system",
			constants.FieldPermissionSet_ProfileID:        "profile-system",
			constants.FieldPermissionSet_IsOwnedByProfile: true,
		},
		strings.ToLower(constants.TableProfile) + ":profile-system": {
			constants.FieldID:               "profile-system",
			constants.FieldProfile_IsSystem: true,
		},
	})
	spec := systemManagedOwnerSpec(
		models.OwnerResolutionHop{
			ViaField: "PermissionSetId",
			ToObject: constants.TablePermissionSet,
			Gate:     &models.OwnerPredicate{Field: constants.FieldPermissionSet_IsOwnedByProfile, Is: true},
		},
		models.OwnerResolutionHop{
			ViaField:   "ProfileId",
			ToObject:   constants.TableProfile,
			SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true},
		},
	)

	if owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, models.SObject{"PermissionSetId": "ps-not-owned"}); err != nil || owned {
		t.Fatalf("gate-fail path: owned=%v err=%v, want false/nil", owned, err)
	}
	if owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, models.SObject{"PermissionSetId": "ps-owned-system"}); err != nil || !owned {
		t.Fatalf("gate-pass path: owned=%v err=%v, want true/nil", owned, err)
	}
}

func TestResolveSystemManagedOwnerOrOfPaths(t *testing.T) {
	// Polymorphic shape: ParentId may reference a PermissionSet (two-hop with
	// gate) or a Profile (single-hop terminal). Either resolving to a system
	// owner means the record is system-managed.
	ps, _ := newPermissionResourceLockService(permissionResourceLockRows())
	spec := &models.OwnerResolutionSpec{Paths: []models.OwnerResolutionPath{
		{Hops: []models.OwnerResolutionHop{
			{ViaField: "ParentId", ToObject: constants.TablePermissionSet, Gate: &models.OwnerPredicate{Field: constants.FieldPermissionSet_IsOwnedByProfile, Is: true}},
			{ViaField: "ProfileId", ToObject: constants.TableProfile, SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true}},
		}},
		{Hops: []models.OwnerResolutionHop{
			{ViaField: "ParentId", ToObject: constants.TableProfile, SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true}},
		}},
	}}

	cases := []struct {
		name   string
		parent string
		owned  bool
	}{
		{"permission set system", "ps-system", true},
		{"permission set custom", "ps-custom", false},
		{"profile system", "profile-system", true},
		{"profile custom", "profile-custom", false},
		{"orphan", "missing", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, models.SObject{"ParentId": tc.parent})
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if owned != tc.owned {
				t.Fatalf("owned = %v, want %v", owned, tc.owned)
			}
		})
	}
}

func TestResolveSystemManagedOwnerSelfHop(t *testing.T) {
	// Self shape: the record itself is the owner candidate (e.g. a
	// PermissionSet's own IsOwnedByProfile flag), followed by a Profile hop.
	ps, _ := newPermissionResourceLockService(permissionResourceLockRows())
	spec := systemManagedOwnerSpec(
		models.OwnerResolutionHop{
			Self: true,
			Gate: &models.OwnerPredicate{Field: constants.FieldPermissionSet_IsOwnedByProfile, Is: true},
		},
		models.OwnerResolutionHop{
			ViaField:   "ProfileId",
			ToObject:   constants.TableProfile,
			SystemWhen: &models.OwnerPredicate{Field: constants.FieldProfile_IsSystem, Is: true},
		},
	)

	systemOwned := models.SObject{
		constants.FieldPermissionSet_IsOwnedByProfile: true,
		"ProfileId": "profile-system",
	}
	notOwned := models.SObject{
		constants.FieldPermissionSet_IsOwnedByProfile: false,
		"ProfileId": "profile-system",
	}

	if owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, systemOwned); err != nil || !owned {
		t.Fatalf("self-owned system: owned=%v err=%v, want true/nil", owned, err)
	}
	if owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, notOwned); err != nil || owned {
		t.Fatalf("self-not-owned: owned=%v err=%v, want false/nil", owned, err)
	}
}

func TestResolveSystemManagedOwnerFindOneErrorPropagates(t *testing.T) {
	boom := errors.New("boom")
	ps := &PersistenceService{repo: &ownerResolutionErrorRepo{err: boom}}
	spec := systemProfileOwnedImmutableOwnerResolutionSpec()

	_, err := ps.resolveSystemManagedOwner(context.Background(), spec, models.SObject{"ProfileId": "profile-system"})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !errors.Is(err, boom) {
		t.Fatalf("expected wrapped boom error, got %v", err)
	}
	if apperrors.GetHTTPStatus(err) != http.StatusInternalServerError {
		t.Fatalf("expected 500, got %d: %v", apperrors.GetHTTPStatus(err), err)
	}
}

func TestResolveSystemManagedOwnerNilSpecAndRecord(t *testing.T) {
	ps := &PersistenceService{repo: &ownerResolutionErrorRepo{}}
	spec := systemProfileOwnedImmutableOwnerResolutionSpec()

	if owned, err := ps.resolveSystemManagedOwner(context.Background(), nil, models.SObject{"ProfileId": "x"}); err != nil || owned {
		t.Fatalf("nil spec: owned=%v err=%v, want false/nil", owned, err)
	}
	if owned, err := ps.resolveSystemManagedOwner(context.Background(), spec, nil); err != nil || owned {
		t.Fatalf("nil record: owned=%v err=%v, want false/nil", owned, err)
	}
}

func TestRejectSystemManagedOwnerWriteChecksBothImages(t *testing.T) {
	ps, _ := newPermissionResourceLockService(permissionResourceLockRows())
	spec := systemProfileOwnedImmutableOwnerResolutionSpec()

	systemRecord := models.SObject{"ProfileId": "profile-system"}
	customRecord := models.SObject{"ProfileId": "profile-custom"}

	// Existing image already system-owned.
	assertPermissionResourceLocked(t, ps.rejectSystemManagedOwnerWrite(context.Background(), spec, systemRecord, customRecord))
	// Final image becomes system-owned.
	assertPermissionResourceLocked(t, ps.rejectSystemManagedOwnerWrite(context.Background(), spec, customRecord, systemRecord))
	// Neither image system-owned: write allowed.
	if err := ps.rejectSystemManagedOwnerWrite(context.Background(), spec, customRecord, customRecord); err != nil {
		t.Fatalf("expected nil error for non-system write, got %v", err)
	}
}
