package system

import (
	"bytes"
	"errors"
	"strings"
	"testing"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// A role that holds a shadowing wildcard plus the permission under test — the
// exact shape that made a revoked permission keep being granted: sales.* covers
// sales.sales-orders.view, so removing only the concrete row leaves the wildcard
// re-granting it.
const (
	revokedPerm = "sales.sales-orders.view"
	wildcardRow = "sales.*"
)

func matrixWithWildcard() *fakeMatrix {
	return &fakeMatrix{
		fakeStore: newFakeStore(),
		perms: []db.ListRolePermissionsRow{
			{Role: "sales", Permission: wildcardRow},
			{Role: "sales", Permission: revokedPerm},
		},
	}
}

func hasPerm(f *fakeMatrix, role, perm string) bool {
	for _, p := range f.perms {
		if p.Role == role && p.Permission == perm {
			return true
		}
	}
	return false
}

func countPerms(f *fakeMatrix, role string) int {
	var n int
	for _, p := range f.perms {
		if p.Role == role {
			n++
		}
	}
	return n
}

// TestSetRolePermissionAbortsOnRemediationReadFailure (A1): the matrix read that
// drives the wildcard remediation used to have its error discarded, so the whole
// shadowing-wildcard fix was skipped without a word. It must now abort the
// revoke, and the wildcard row must survive — a committed revoke the operator
// asked for is better than a revoke silently left shadowed by sales.*.
func TestSetRolePermissionAbortsOnRemediationReadFailure(t *testing.T) {
	fm := matrixWithWildcard()
	fm.failRead = true
	s := newTestService(fm)

	err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", revokedPerm, false)
	if err == nil {
		t.Fatal("a failed remediation read must be reported, not swallowed")
	}
	if !strings.Contains(err.Error(), "matrix read failed") {
		t.Errorf("err = %v, want the read failure to survive the wrap", err)
	}
	if fm.lastTx.committed {
		t.Error("tx committed despite the failed read")
	}
	if !fm.lastTx.rolledBack {
		t.Error("tx was not rolled back")
	}
	// Rollback must undo the concrete revoke taken before the read: the block is
	// all-or-nothing, and the pre-existing state is the truthful one.
	if !hasPerm(fm, "sales", revokedPerm) {
		t.Errorf("concrete row lost after rollback: %+v", fm.perms)
	}
	if !hasPerm(fm, "sales", wildcardRow) {
		t.Errorf("wildcard row must survive a failed remediation: %+v", fm.perms)
	}
	if len(fm.deleteCalls) != 1 || fm.deleteCalls[0] != revokedPerm {
		t.Errorf("the concrete revoke is the only delete allowed, got %v", fm.deleteCalls)
	}
	if len(fm.upsertCalls) != 0 {
		t.Errorf("no expansion may run without a readable matrix, got %v", fm.upsertCalls)
	}
}

// TestSetRolePermissionAbortsOnSiblingUpsertFailure (A3): a failed expansion
// upsert used to be ignored while the wildcard row was deleted anyway, so the
// role silently lost every other permission under that prefix. It must abort
// before the delete.
func TestSetRolePermissionAbortsOnSiblingUpsertFailure(t *testing.T) {
	fm := matrixWithWildcard()
	fm.failUpsert = true
	s := newTestService(fm)

	err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", revokedPerm, false)
	if err == nil {
		t.Fatal("a failed expansion upsert must be reported, not swallowed")
	}
	if !strings.Contains(err.Error(), "upsert failed") {
		t.Errorf("err = %v, want the upsert failure to survive the wrap", err)
	}
	if fm.lastTx.committed {
		t.Error("tx committed despite the failed upsert")
	}
	if !fm.lastTx.rolledBack {
		t.Error("tx was not rolled back")
	}
	if !hasPerm(fm, "sales", revokedPerm) || !hasPerm(fm, "sales", wildcardRow) {
		t.Errorf("rollback must restore the pre-block rows: %+v", fm.perms)
	}
	// The over-revocation itself: the wildcard must never be deleted when its
	// expansion failed.
	for _, perm := range fm.deleteCalls {
		if strings.HasSuffix(perm, ".*") {
			t.Fatalf("wildcard %q deleted after a failed expansion: calls %v", perm, fm.deleteCalls)
		}
	}
}

// TestSetRolePermissionDeleteErrorAborts (A2): the wildcard delete used to
// discard its error too, so a failing delete was reported as a clean revoke.
func TestSetRolePermissionDeleteErrorAborts(t *testing.T) {
	fm := matrixWithWildcard()
	fm.deleteErrPerm = wildcardRow
	s := newTestService(fm)

	err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", revokedPerm, false)
	if err == nil {
		t.Fatal("a failed wildcard delete must be reported, not swallowed")
	}
	if !strings.Contains(err.Error(), "delete failed") {
		t.Errorf("err = %v, want the delete failure to survive the wrap", err)
	}
	if fm.lastTx.committed {
		t.Error("tx committed despite the failed delete")
	}
	if !hasPerm(fm, "sales", revokedPerm) || !hasPerm(fm, "sales", wildcardRow) {
		t.Errorf("rollback must restore the pre-block rows: %+v", fm.perms)
	}
}

// TestSetRolePermissionZeroRowWildcardDeleteIsNotAnError: zero rows is
// legitimately possible, so it must not be turned into a hard failure — but it
// must be visible, as a WARN, on the same precedent as the token-revocation fix.
func TestSetRolePermissionZeroRowWildcardDeleteIsNotAnError(t *testing.T) {
	fm := matrixWithWildcard()
	fm.deleteWildcardZero = true
	s := newTestService(fm)

	var buf bytes.Buffer
	if err := s.SetRolePermission(loggedCtx(&buf), "sales", revokedPerm, false); err != nil {
		t.Fatalf("a zero-row delete must not fail the revoke: %v", err)
	}
	if !fm.lastTx.committed {
		t.Error("tx must commit on the zero-row path")
	}
	if hasPerm(fm, "sales", revokedPerm) {
		t.Errorf("the concrete revoke must still stand: %+v", fm.perms)
	}

	out := buf.String()
	if !strings.Contains(out, "wildcard role-permission delete matched zero rows") {
		t.Errorf("zero-row delete not logged: %q", out)
	}
	if !strings.Contains(out, "level=WARN") {
		t.Errorf("zero-row delete must log at WARN: %q", out)
	}
	if !strings.Contains(out, "role=sales") || !strings.Contains(out, "permission="+wildcardRow) {
		t.Errorf("log must name the role and wildcard: %q", out)
	}
}

// TestSetRolePermissionHappyPathUnchanged: nothing above may have altered the
// successful path — revoke, expand the wildcard, drop it, commit, notify.
func TestSetRolePermissionHappyPathUnchanged(t *testing.T) {
	fm := matrixWithWildcard()
	s := newTestService(fm)
	notified := 0
	s.SetOnMatrixChange(func() { notified++ })

	if err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", revokedPerm, false); err != nil {
		t.Fatalf("revoke: %v", err)
	}
	if hasPerm(fm, "sales", revokedPerm) {
		t.Errorf("concrete permission still granted: %+v", fm.perms)
	}
	if hasPerm(fm, "sales", wildcardRow) {
		t.Errorf("shadowing wildcard still present: %+v", fm.perms)
	}
	// The role keeps every sibling it did not lose: the expansion is what
	// preserves them, and over-revocation is the A3 failure mode.
	if !hasPerm(fm, "sales", "sales.sales-orders.create") {
		t.Errorf("expansion lost a sibling permission: %+v", fm.perms)
	}
	if got := countPerms(fm, "sales"); got < 2 {
		t.Errorf("role lost too many permissions: %d rows left in %+v", got, fm.perms)
	}
	if !fm.lastTx.committed {
		t.Error("happy path must commit")
	}
	if fm.lastTx.rolledBack {
		t.Error("happy path must not roll back")
	}
	if notified != 1 {
		t.Errorf("matrix notifications = %d, want 1", notified)
	}

	// A grant still lands in its own transaction.
	if err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", revokedPerm, true); err != nil {
		t.Fatalf("grant: %v", err)
	}
	if !hasPerm(fm, "sales", revokedPerm) {
		t.Errorf("grant did not persist: %+v", fm.perms)
	}
	if notified != 2 {
		t.Errorf("matrix notifications = %d, want 2", notified)
	}
}

// TestSetRolePermissionRejectsBeforeOpeningATransaction: validation still runs
// first, so a bad key never burns a transaction.
func TestSetRolePermissionRejectsBeforeOpeningATransaction(t *testing.T) {
	fm := matrixWithWildcard()
	s := newTestService(fm)

	err := s.SetRolePermission(loggedCtx(&bytes.Buffer{}), "sales", "inventory.stock.read", false)
	if !errors.Is(err, ErrUnknownPermission) {
		t.Fatalf("err = %v, want ErrUnknownPermission", err)
	}
	if fm.lastTx != nil {
		t.Error("a rejected permission must not open a transaction")
	}
}
