package permissions

import (
	"context"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func strp(s string) *string { return &s }

// ValidatePermissionPrerequisites is the merged-final-state checker: given
// current grants + proposed changes, it validates each catalog row's
// RequiresPermission pointer against the union. A transitive A→B→C chain
// is already handled by the single-pass sweep — if C is disabled, both B
// (requires C) and A (requires B) are flagged on their respective rows.
func TestValidatePermissionPrerequisites_RejectsMissingPrereq(t *testing.T) {
	ctx := context.Background()
	repo := &mockPermRepo{
		listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
			return []models.SystemPermission{
				{APIName: "ManageUsers", Label: "Manage Users", RequiresPermission: strp("ViewSetup"), IsActive: true},
				{APIName: "ViewSetup", Label: "View Setup", IsActive: true},
			}, nil
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	// Enable ManageUsers with ViewSetup absent — reject.
	err := ps.ValidatePermissionPrerequisites(ctx, map[string]bool{}, map[string]bool{"ManageUsers": true})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "ManageUsers requires ViewSetup")

	// Enable both in the same batch — final state satisfies the requirement.
	err = ps.ValidatePermissionPrerequisites(ctx, map[string]bool{}, map[string]bool{"ManageUsers": true, "ViewSetup": true})
	require.NoError(t, err)

	// ViewSetup already granted; enabling ManageUsers now is fine.
	err = ps.ValidatePermissionPrerequisites(ctx, map[string]bool{"ViewSetup": true}, map[string]bool{"ManageUsers": true})
	require.NoError(t, err)

	// Revoking ViewSetup while ManageUsers is still enabled in final state — reject.
	err = ps.ValidatePermissionPrerequisites(ctx, map[string]bool{"ManageUsers": true, "ViewSetup": true}, map[string]bool{"ViewSetup": false})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "ManageUsers requires ViewSetup")
}

// A→B→C transitive chain: enabling A while B is off and C is off should
// surface the closest unsatisfied link (A→B). Enabling B too (leaving C off)
// should then surface B→C. Enabling all three is accepted.
func TestValidatePermissionPrerequisites_TransitiveChain(t *testing.T) {
	ctx := context.Background()
	repo := &mockPermRepo{
		listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
			return []models.SystemPermission{
				{APIName: "A", RequiresPermission: strp("B"), IsActive: true},
				{APIName: "B", RequiresPermission: strp("C"), IsActive: true},
				{APIName: "C", IsActive: true},
			}, nil
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	err := ps.ValidatePermissionPrerequisites(ctx, map[string]bool{}, map[string]bool{"A": true})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "A requires B")

	err = ps.ValidatePermissionPrerequisites(ctx, map[string]bool{}, map[string]bool{"A": true, "B": true})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "B requires C")

	err = ps.ValidatePermissionPrerequisites(ctx, map[string]bool{}, map[string]bool{"A": true, "B": true, "C": true})
	require.NoError(t, err)
}

// ApplyPermissionChanges is the W1 gate: an unknown apiName is rejected
// before any junction write. Test uses a no-op repo that would panic on
// SetSystemPermission — proving no write was attempted.
type panicOnWritePermRepo struct {
	mockPermRepo
}

func (p *panicOnWritePermRepo) SetSystemPermission(_ context.Context, _, _ string, _ bool) error {
	panic("SetSystemPermission called despite unknown-apiName validation failure")
}

func TestApplyPermissionChanges_RejectsUnknownAPIName(t *testing.T) {
	ctx := context.Background()
	repo := &panicOnWritePermRepo{
		mockPermRepo: mockPermRepo{
			listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
				return []models.SystemPermission{
					{APIName: "ApiEnabled", IsActive: true},
				}, nil
			},
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	err := ps.ApplyPermissionChanges(ctx, "ps-1", nil, map[string]bool{"Foobar": true}, nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "unknown permission: Foobar")

	// A known permission flip succeeds (but this mock's SetSystemPermission
	// would panic — so we can't flip true; test only the rejection path).
	err = ps.ApplyPermissionChanges(ctx, "ps-1", nil, map[string]bool{}, nil)
	require.NoError(t, err)
}

// P7 ordering: unknown apiName rejection runs before diff computation, so
// even a request that would be a no-op under the current grants still
// rejects an unknown name. Guards PS-004's contract against the
// "diff-first" optimization trap: if diff were computed first, a
// PATCH {PermissionsFoobar: false} where Foobar is already false would
// silently succeed without the catalog check.
func TestApplyPermissionChanges_RejectsUnknownAPINameEvenOnNoOpValue(t *testing.T) {
	ctx := context.Background()
	repo := &panicOnWritePermRepo{
		mockPermRepo: mockPermRepo{
			listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
				return []models.SystemPermission{
					{APIName: "ApiEnabled", IsActive: true},
				}, nil
			},
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	// Foobar is unknown, requested value matches the default (false) — this
	// is a "no-op-value" request. Must still be rejected by catalog check.
	err := ps.ApplyPermissionChanges(ctx, "ps-1", map[string]bool{"Foobar": false}, map[string]bool{"Foobar": false}, nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "unknown permission: Foobar")
}

// P7 ordering: a genuine no-op PATCH (every key matches current grants) on
// known permissions must succeed even if the existing grant set already
// violates a newly-added prerequisite chain. Rationale: prerequisite
// chains are properties of the intended final state, and since the diff
// is empty, the final state equals the already-persisted state and the
// write is a logical no-op — a pre-existing violation is not this
// request's responsibility. Enforces the non-regression relative to
// pre-refactor behavior for admin UIs that send the full grant set on
// every save.
func TestApplyPermissionChanges_NoOpPATCHBypassesPrereqCheck(t *testing.T) {
	ctx := context.Background()
	// Catalog: ModifyAllData requires ViewAllData. Current state has
	// ModifyAllData=true but ViewAllData=false — a pre-existing chain
	// violation introduced by a seeder bug or a historical import.
	repo := &panicOnWritePermRepo{
		mockPermRepo: mockPermRepo{
			listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
				return []models.SystemPermission{
					{APIName: "ModifyAllData", RequiresPermission: strp("ViewAllData"), IsActive: true},
					{APIName: "ViewAllData", IsActive: true},
				}, nil
			},
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	current := map[string]bool{"ModifyAllData": true, "ViewAllData": false}

	// Request values match current → diff is empty → must succeed,
	// bypassing the prereq check (which would otherwise reject the pre-
	// existing violation).
	err := ps.ApplyPermissionChanges(ctx, "ps-1", current, map[string]bool{
		"ModifyAllData": true,
		"ViewAllData":   false,
	}, nil)
	require.NoError(t, err)
}

// P7 ordering: when the diff is non-empty, prereq validation runs on
// the merged final state. A diff that would flip ViewAllData=false while
// ModifyAllData=true stays true must reject, because the final state
// violates the chain.
func TestApplyPermissionChanges_DiffFlipRejectsOnPrereqViolation(t *testing.T) {
	ctx := context.Background()
	repo := &panicOnWritePermRepo{
		mockPermRepo: mockPermRepo{
			listSystemPermissions: func(_ context.Context) ([]models.SystemPermission, error) {
				return []models.SystemPermission{
					{APIName: "ModifyAllData", RequiresPermission: strp("ViewAllData"), IsActive: true},
					{APIName: "ViewAllData", IsActive: true},
				}, nil
			},
		},
	}
	ps := NewPermissionService(repo, nil, nil)

	// Current: both granted. Request: revoke ViewAllData only. Merged
	// final state: ModifyAllData=true, ViewAllData=false → chain violated.
	err := ps.ApplyPermissionChanges(ctx, "ps-1",
		map[string]bool{"ModifyAllData": true, "ViewAllData": true},
		map[string]bool{"ViewAllData": false},
		nil,
	)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "ModifyAllData requires ViewAllData")
}
