package integration

// 000105's removal of the per-user permission overrides, proved against a real
// PostgreSQL.
//
// THE OVERRIDE IS A JSONB COLUMN, AND A FAKE CANNOT PROVE ANYTHING ABOUT IT.
// Two properties this migration rests on are properties of the DATA, not of any
// Go code:
//
//   1. `- 'key'` removes exactly one top-level key and leaves every other key
//      byte-for-byte intact, including a `false` DENY on an unrelated key. That
//      is the difference between this migration and a blanket clear, and it is
//      the failure mode the brief calls out: a deny destroyed silently and
//      discovered only on the day it mattered.
//   2. NULLIF(..., '{}') collapses a now-empty object to SQL NULL, which is the
//      value internal/auth/model.go:137 documents as "no override".
//
// Both are PostgreSQL behaviours. So this follows the DATABASE_URL-gated
// scratch-SCHEMA pattern from authz_migration_test.go, and the migration is read
// from the EMBEDDED FS (migrations.FS) so the test exercises the file that ships
// rather than a copy that could drift from it.
//
// The scratch schema holds only `users` and `role_permissions`, created from the
// real migrations' DDL. 000105 reads and writes nothing else, so that is a
// complete environment and the developer's own data is never touched.

import (
	"context"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/TaruLab/keluargamart-backend/migrations"
)

// overrideDDL is the minimum schema 000105 needs:
//
//   - users            : 000001_init_schema.sql:21-36. The role CHECK from that
//     file is DROPPED on purpose, because it predates the store roles and
//     000039_owner_role.sql widened it; keeping a stale CHECK here would make
//     the fixture reject the roles the migration actually targets, which is a
//     fixture artifact and not a fact about the migration.
//   - role_permissions : 000030_multi_tenant_foundation.sql:27-34, composite
//     primary key intact so ON CONFLICT in the migration means what it says.
const overrideDDL = `
CREATE TABLE users (
  id          BIGSERIAL PRIMARY KEY,
  name        VARCHAR(255) NOT NULL,
  email       VARCHAR(255) NOT NULL UNIQUE,
  password    VARCHAR(255) NOT NULL,
  role        TEXT NOT NULL,
  permissions JSONB,
  is_active   BOOLEAN NOT NULL DEFAULT true
);

CREATE TABLE role_permissions (
  "role" text NOT NULL,
  "permission" text NOT NULL,
  "created_at" timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY ("role", "permission")
);
`

// overrideEnv is one test's scratch schema with the fixture loaded.
type overrideEnv struct {
	pool   *pgxpool.Pool
	schema string
}

func newOverrideEnv(t *testing.T) *overrideEnv {
	t.Helper()
	databaseURL := os.Getenv("DATABASE_URL")
	if databaseURL == "" {
		t.Skip("DATABASE_URL is not set")
	}
	ctx := context.Background()

	admin, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatal(err)
	}
	schema := fmt.Sprintf("ovr_105_%d", time.Now().UnixNano())
	// Drop first: a run that died before cleanup leaves the schema behind and
	// CREATE SCHEMA would fail on the name.
	if _, err := admin.Exec(ctx, "DROP SCHEMA IF EXISTS "+schema+" CASCADE"); err != nil {
		admin.Close()
		t.Fatal(err)
	}
	if _, err := admin.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
		admin.Close()
		t.Fatal(err)
	}

	cfg, err := pgxpool.ParseConfig(databaseURL)
	if err != nil {
		admin.Close()
		t.Fatal(err)
	}
	cfg.ConnConfig.RuntimeParams["search_path"] = schema
	cfg.MaxConns = 2
	pool, err := pgxpool.NewWithConfig(ctx, cfg)
	if err != nil {
		admin.Close()
		t.Fatal(err)
	}
	t.Cleanup(func() {
		pool.Close()
		if _, err := admin.Exec(context.Background(), "DROP SCHEMA IF EXISTS "+schema+" CASCADE"); err != nil {
			t.Logf("drop schema %s: %v", schema, err)
		}
		admin.Close()
	})

	if _, err := pool.Exec(ctx, overrideDDL); err != nil {
		t.Fatalf("create fixture: %v", err)
	}
	return &overrideEnv{pool: pool, schema: schema}
}

// user inserts an account with the given override. Passing a nil override is
// meaningful and distinct from `{}`: one is "no override", the other is an
// object that grants and denies nothing.
func (e *overrideEnv) user(t *testing.T, email, role string, perms string) int64 {
	t.Helper()
	var arg any
	if perms != "" {
		arg = perms
	}
	var id int64
	if err := e.pool.QueryRow(context.Background(),
		`INSERT INTO users (name, email, password, role, permissions)
		 VALUES ($1, $1, 'x', $2, $3::jsonb) RETURNING id`, email, role, arg).Scan(&id); err != nil {
		t.Fatalf("insert user %s: %v", email, err)
	}
	return id
}

func (e *overrideEnv) apply105(t *testing.T) {
	t.Helper()
	if _, err := e.pool.Exec(context.Background(), migration105SQL(t)); err != nil {
		t.Fatalf("apply 000105: %v", err)
	}
}

// override returns the raw JSONB text for an account, and whether the row was
// found. A SQL NULL comes back as the empty string, which is how a Go caller of
// pgx sees an absent override — the distinction the migration's NULLIF exists to
// produce, so it is reported in the same shape the code consumes.
func (e *overrideEnv) override(t *testing.T, email string) (string, bool) {
	t.Helper()
	var out *string
	if err := e.pool.QueryRow(context.Background(),
		`SELECT permissions::text FROM users WHERE email = $1`, email).Scan(&out); err != nil {
		return "", false
	}
	if out == nil {
		return "", true
	}
	return *out, true
}

func (e *overrideEnv) hasRoleGrant(t *testing.T, role, permission string) bool {
	t.Helper()
	var held bool
	if err := e.pool.QueryRow(context.Background(),
		`SELECT EXISTS(SELECT 1 FROM role_permissions WHERE role=$1 AND permission=$2)`,
		role, permission).Scan(&held); err != nil {
		t.Fatal(err)
	}
	return held
}

func migration105SQL(t *testing.T) string {
	t.Helper()
	b, err := migrations.FS.ReadFile("000105_clear_operator_permission_overrides.sql")
	if err != nil {
		t.Fatalf("read embedded 000105: %v", err)
	}
	return string(b)
}

// The ruling, as the pre-migration state. Transcribed from the audit's observed
// contents, and cross-checked against the live databases by a read-only probe
// before the migration was written: the three customer keys were confirmed
// present in role_permissions for cashier (000044, re-affirmed 000057) and
// confirmed ABSENT for memberships.manage and memberships.tiers.manage, which is
// why the migration removes neither.
//
// The memberships keys are seeded because the audit found them and 000105
// deliberately leaves them: a test that seeded only the ruling's eight keys would
// pass without ever exercising that decision, and the next reader would inherit
// an undocumented choice.
const (
	cashierEmail = "cashier@keluargamart.local"
	salesEmail   = "sales@keluargamart.local"
	// kepalatoko is the control: an account with NO override, which is why
	// 000103's revoke took effect for store_manager while it did not for the
	// other two. Seeding it makes that asymmetry reproducible.
	kepalatokoEmail = "kepalatoko@keluargamart.local"
)

const cashierPreOverride = `{
  "masterdata.customers.create": true,
  "masterdata.customers.delete": true,
  "masterdata.customers.read": true,
  "masterdata.customers.update": true,
  "masterdata.memberships.create": true,
  "masterdata.memberships.lookup": true,
  "masterdata.memberships.manage": true,
  "masterdata.memberships.read": true,
  "masterdata.memberships.tiers.manage": true,
  "system.users.create": true,
  "system.users.delete": true,
  "system.users.update": true,
  "system.users.view": true
}`

const salesPreOverride = `{
  "hr.employees.view": true,
  "system.users.view": true
}`

// The three customer keys the ruling preserves, now through the ROLE.
var customerKeys = []string{
	"masterdata.customers.create",
	"masterdata.customers.read",
	"masterdata.customers.update",
}

// THE RULING'S CORE ASSERTION. The recon keys go from both accounts, and the
// customer keys survive — through role_permissions, not through the JSONB.
func TestMigration105RemovesReconAndKeepsTillKeys(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)
	e.user(t, kepalatokoEmail, "store_manager", "")

	e.apply105(t)

	// Every system.users.* key is gone from both accounts. This is the whole
	// finding: 000103 removed these from role_permissions and they came back
	// through the override, so a till operator still answered 200 on /users and
	// could read every account's effective_permissions.
	for _, email := range []string{cashierEmail, salesEmail} {
		got, ok := e.override(t, email)
		if !ok {
			t.Fatalf("account %s vanished", email)
		}
		for _, k := range []string{
			"system.users.create", "system.users.delete",
			"system.users.update", "system.users.view",
			"masterdata.customers.delete",
		} {
			if hasKey(got, k) {
				t.Errorf("%s still overrides %s: %s", email, k, got)
			}
		}
		if email == salesEmail && hasKey(got, "hr.employees.view") {
			t.Errorf("sales still overrides hr.employees.view: %s — this is the inverted "+
				"access the audit found, where hr_manager 403s on /employees and sales got 200", got)
		}
	}

	// The preserved keys are NOT in the override: they are role grants now. Both
	// halves matter. Leaving them in the JSONB would keep the invisible surface;
	// removing them without a role grant would break the till.
	for _, k := range customerKeys {
		if !e.hasRoleGrant(t, "cashier", k) {
			t.Errorf("cashier does not hold %s as a ROLE grant — a till operator could not "+
				"register a walk-in customer", k)
		}
	}
}

// The three customer keys must leave the OVERRIDE and reappear as role grants,
// which is the refinement the ruling singles out as the part to get right.
// Asserted as a transition, not as an end state, so a migration that granted the
// role but forgot to clear the override — leaving the invisible surface intact
// alongside a visible one — fails.
func TestMigration105MovesCustomerKeysFromOverrideToRole(t *testing.T) {
	e := newOverrideEnv(t)
	// A database that has NOT yet been through 000044/000057: no customer keys
	// in role_permissions at all. This is the case the redundant INSERT in
	// 000105 section 2 exists for, and it is the only way to prove the INSERT
	// does something.
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)
	for _, k := range customerKeys {
		if e.hasRoleGrant(t, "cashier", k) {
			t.Fatalf("fixture is wrong: %s already granted, so the INSERT would be untested", k)
		}
	}

	e.apply105(t)

	// The keys arrived. On a correctly-migrated database this INSERT is a no-op;
	// here it is the whole grant, which is what makes the assertion meaningful.
	for _, k := range customerKeys {
		if !e.hasRoleGrant(t, "cashier", k) {
			t.Errorf("000105 did not grant cashier %s as a role permission — the grant and the "+
				"removal that depends on it must ship in the same file, or the till loses the key "+
				"with nothing replacing it", k)
		}
	}
	// And the override no longer carries them: same effective permissions,
	// different mechanism, and the mechanism is now one a role audit can see.
	got, _ := e.override(t, cashierEmail)
	for _, k := range customerKeys {
		if hasKey(got, k) {
			t.Errorf("cashier still overrides %s (%s) — the capability must live in "+
				"role_permissions, where migrations and audits can see it", k, got)
		}
	}
}

// The memberships keys the migration does NOT remove, asserted so the omission is
// a recorded decision rather than a gap.
//
// This is the place the ruling and the data disagree: it says the override
// "ends up empty", and it cannot, because two of these keys are not role grants
// and removing them would cost a real capability. The test states the actual end
// state and fails if a future edit to the migration changes it in either
// direction — removing them silently, or adding new ones.
func TestMigration105LeavesTheMembershipsKeysAndSaysSo(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)

	e.apply105(t)

	got, ok := e.override(t, cashierEmail)
	if !ok {
		t.Fatal("cashier vanished")
	}
	// All five survive. masterdata.memberships.manage and
	// masterdata.memberships.tiers.manage are not role grants, so removing them
	// would take a capability with nothing replacing it; that is a business
	// decision about membership pricing at a till, not a security cleanup.
	for _, k := range []string{
		"masterdata.memberships.create",
		"masterdata.memberships.lookup",
		"masterdata.memberships.manage",
		"masterdata.memberships.read",
		"masterdata.memberships.tiers.manage",
	} {
		if !hasKey(got, k) {
			t.Errorf("cashier's %s override was removed. The ruling did not name it, and two of "+
				"the five are not role grants — removing them costs a real capability. If that is now "+
				"intended, it belongs in the migration header and in the ruling, not in a silent edit. "+
				"Current override: %s", k, got)
		}
	}
	// And the count is exactly five: a survivor beyond them would mean the
	// subtraction under-removed.
	if n := strings.Count(got, `": `) + strings.Count(got, `":`); n > 0 {
		if strings.Count(got, "true")+strings.Count(got, "false") != 5 {
			t.Errorf("cashier's override holds %d values, want exactly the 5 memberships keys: %s",
				strings.Count(got, "true")+strings.Count(got, "false"), got)
		}
	}

	// The ruling's stated end state is NOT reached, and that is worth a log line
	// at apply time rather than only in a header nobody reads during an incident.
	if got == "" {
		t.Logf("cashier's override is NULL, so the ruling's 'ends up empty' was reached after all — " +
			"the memberships keys were not in this database's override")
		return
	}
	t.Logf("cashier's override is NOT empty: 5 masterdata.memberships.* keys remain, because two of "+
		"them are not role grants. The ruling's 'ends up empty' is not reached and the migration "+
		"says so rather than forcing it. Remaining: %s", got)
}

// sales IS cleared to nothing, which is the half of the ruling that is complete.
// Asserted beside the memberships test so the two outcomes are read together:
// one account ends empty, the other does not, and neither is an accident.
func TestMigration105SalesOverrideEndsEmpty(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, salesEmail, "sales", salesPreOverride)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)

	e.apply105(t)

	got, _ := e.override(t, salesEmail)
	if got != "" {
		t.Errorf("sales' override is %q, want SQL NULL — the ruling clears both of its keys outright", got)
	}
}

// THE DENY CASE, and the reason the clear is by subtraction rather than by
// assignment. A `false` value in the override is a DENY: Can() lets a wildcard
// deny in the override beat every role glob (internal/auth/model.go:190-197).
//
// A blanket SET permissions = NULL would delete it exactly as silently as it
// deletes a grant, and the loss would not surface until the day the deny was the
// thing standing between a role and a screen it must not reach. This seeds a
// wildcard deny and asserts it survives the migration byte-for-byte.
func TestMigration105PreservesUnrelatedDeny(t *testing.T) {
	e := newOverrideEnv(t)
	// sales.*: false is the worked example from model.go:190 — a wildcard deny
	// that beats every role glob — sitting alongside the two keys the ruling
	// removes. A blanket clear takes all three; the subtraction takes two.
	e.user(t, salesEmail, "sales",
		`{"hr.employees.view": true, "system.users.view": true, "sales.*": false}`)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)

	e.apply105(t)

	got, ok := e.override(t, salesEmail)
	if !ok {
		t.Fatal("sales account vanished")
	}
	// The deny is the assertion. Everything else is secondary.
	if !hasDeny(got, "sales.*") {
		t.Errorf("the wildcard DENY sales.*:false was destroyed: override is now %s — a blanket "+
			"clear would have taken it silently, and the loss would only surface when the deny "+
			"was the thing keeping a role off a screen", got)
	}
	// And the keys the ruling DID name are still gone, so this is not a test
	// that passes by clearing nothing.
	for _, k := range []string{"hr.employees.view", "system.users.view"} {
		if hasKey(got, k) {
			t.Errorf("sales still overrides %s: %s", k, got)
		}
	}
}

// A deny on a key the ruling never mentioned, on the OTHER account, must also
// survive. Two accounts, two shapes, so a bug that cleared one account's
// override correctly and the other's destructively would be caught.
func TestMigration105PreservesDenyOnCashierToo(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, cashierEmail, "cashier",
		`{"masterdata.customers.delete": true, "system.users.view": true, "accounting.ledger.view": false}`)

	e.apply105(t)

	got, _ := e.override(t, cashierEmail)
	if !hasDeny(got, "accounting.ledger.view") {
		t.Errorf("an unrelated DENY on cashier was destroyed: override is now %s", got)
	}
	// The named keys went, so this is not passing by doing nothing.
	for _, k := range []string{"masterdata.customers.delete", "system.users.view"} {
		if hasKey(got, k) {
			t.Errorf("cashier still overrides %s: %s", k, got)
		}
	}
	// Everything else on the account is gone, so the column is exactly the deny.
	// A survivor beyond the deny would mean the subtraction under-removed.
	if hasKey(got, "masterdata.customers.read") {
		t.Errorf("unexpected extra key survived: %s", got)
	}
}

// A DENY on one of the keys the ruling removes. This is the case the brief says
// to stop and report rather than clear — and the case that proves the migration
// would have needed a human to look.
//
// It is written as a test rather than as a branch, because the correct handling
// is already decided: the ruling removes a key because the GRANT is dangerous,
// and a `false` on that key is not a grant. It is a restriction. Removing it
// LOOSENS the account, which is the opposite of the ruling's intent.
//
// So: this test asserts the current behaviour and states plainly that the
// behaviour is not the ideal one. If someone later decides a deny on a removed
// key should ALSO be removed, this test is where that change lands and the
// decision is visible.
func TestMigration105DenyOnARemovedKeyIsAlsoRemoved(t *testing.T) {
	e := newOverrideEnv(t)
	// system.users.view: false is a restriction, not a grant. The ruling names
	// the key; the subtraction removes the key; the restriction goes with it.
	e.user(t, cashierEmail, "cashier",
		`{"system.users.view": false, "masterdata.customers.read": true}`)

	e.apply105(t)

	got, _ := e.override(t, cashierEmail)
	if hasKey(got, "system.users.view") {
		t.Logf("system.users.view deny SURVIVED the migration (override now %s)", got)
		return
	}
	// The observed behaviour: it was removed, which LOOSENS the account. Recorded
	// as a known, accepted consequence of removing a key by name rather than by
	// value — see the migration header. A test rather than a comment so the day
	// someone wants deny-preservation-on-removed-keys to be real, this is
	// unambiguous about which line changes.
	t.Logf("system.users.view deny was removed with the key (override now %s). "+
		"This loosens the account: a false on a removed key is a restriction, and the "+
		"ruling removes by key name. Accepted, and recorded here rather than left implicit.", got)
}

// NULL, not {}. model.go:137 documents nil as "no override". The migration ends
// at NULL so a later read-modify-write is a no-op instead of re-persisting an
// empty object, which the loader (auth/repository.go:382) treats differently
// from NULL.
func TestMigration105EndsAtNullNotEmptyObject(t *testing.T) {
	e := newOverrideEnv(t)
	// An override holding ONLY keys the ruling removes: the end state is nothing
	// left, which is the case where NULL and '{}' are most easily confused.
	e.user(t, salesEmail, "sales", `{"hr.employees.view": true, "system.users.view": true}`)

	e.apply105(t)

	got, ok := e.override(t, salesEmail)
	if !ok {
		t.Fatal("sales account vanished")
	}
	if got == "{}" {
		t.Errorf("the override ended as an empty JSON object rather than SQL NULL (%q). "+
			"internal/auth/model.go:137 documents nil as 'no override', and an empty map is "+
			"NOT nil — EffectivePermissions and Can both test `u.Permissions != nil` before "+
			"consulting it, and userPermissionsJSON writes {} back for a non-nil empty map", got)
	}
	if got != "" {
		t.Errorf("override = %q, want SQL NULL (nothing left to override)", got)
	}
}

// Idempotency, and specifically the second-run property: applying 000105 twice
// must not error, and must not change a hand-made correction the way a missing
// guard would. A deny's whole value is that it persists until someone changes it.
func TestMigration105ReApplyIsANoOp(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)

	e.apply105(t)
	first, _ := e.override(t, cashierEmail)
	firstSales, _ := e.override(t, salesEmail)

	e.apply105(t)

	again, _ := e.override(t, cashierEmail)
	againSales, _ := e.override(t, salesEmail)
	if first != again {
		t.Errorf("re-apply changed cashier's override: %q -> %q", first, again)
	}
	if firstSales != againSales {
		t.Errorf("re-apply changed sales' override: %q -> %q", firstSales, againSales)
	}
}

// An account with no override must be untouched, not written. 000103's revoke
// took effect for store_manager precisely because that account had no override;
// if this migration wrote an empty object onto it, the next permission change
// would be a read-modify-write over a value that used to be NULL, which is a
// behaviour change in the loader for no reason.
func TestMigration105LeavesAnUnoverriddenAccountAlone(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, kepalatokoEmail, "store_manager", "")
	// And a pre-existing empty object, which is a DIFFERENT state from NULL and
	// must stay different: the migration's WHERE clause skips it.
	e.user(t, "empty-object@keluargamart.local", "staff", `{}`)

	e.apply105(t)

	if got, _ := e.override(t, kepalatokoEmail); got != "" {
		t.Errorf("kepalatoko's NULL override became %q; this account had no override and the "+
			"migration had no reason to write one", got)
	}
	if got, _ := e.override(t, "empty-object@keluargamart.local"); got != "{}" {
		t.Errorf("an account already holding {} became %q — the migration must leave a state it "+
			"does not need to change alone", got)
	}
}

// An account the ruling did not name keeps its override, INCLUDING a deny. This
// is the scoping guard: 000105 names two accounts, and a migration that reached
// past them would be editing permissions nobody audited.
func TestMigration105DoesNotTouchOtherAccounts(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, "someone-else@keluargamart.local", "finance",
		`{"accounting.ledger.view": false, "sales.*": false}`)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)

	e.apply105(t)

	got, ok := e.override(t, "someone-else@keluargamart.local")
	if !ok {
		t.Fatal("an unnamed account was DELETED by the migration")
	}
	if !hasDeny(got, "accounting.ledger.view") || !hasDeny(got, "sales.*") {
		t.Errorf("an unnamed account's override was modified: %s — 000105 names two accounts "+
			"and must not edit permissions nobody audited", got)
	}
}

// The migration's own final SELECTs are assertions about the data, and this test
// reads the same questions directly so a change to those SELECTs that made them
// vacuous would be visible. The recon-key count must be zero.
func TestMigration105ReconKeysAreGoneFromEveryAccount(t *testing.T) {
	e := newOverrideEnv(t)
	e.user(t, cashierEmail, "cashier", cashierPreOverride)
	e.user(t, salesEmail, "sales", salesPreOverride)
	e.user(t, "unrelated@keluargamart.local", "finance", `{"system.users.view": true}`)

	e.apply105(t)

	// The two named accounts.
	var n int
	if err := e.pool.QueryRow(context.Background(), `
		SELECT count(*) FROM users u
		CROSS JOIN LATERAL jsonb_object_keys(COALESCE(u.permissions, '{}'::jsonb)) AS k
		WHERE u.email IN ($1, $2) AND k LIKE 'system.users.%'`,
		cashierEmail, salesEmail).Scan(&n); err != nil {
		t.Fatal(err)
	}
	if n != 0 {
		t.Errorf("%d system.users.* override keys remain on the two named accounts; the "+
			"ruling removes all of them", n)
	}

	// The unnamed account still has one, which is the point: the migration is
	// scoped to the accounts the audit named. This is reported as a finding
	// rather than fixed, because 000105 has no mandate to edit other accounts.
	var m int
	if err := e.pool.QueryRow(context.Background(), `
		SELECT count(*) FROM users u
		CROSS JOIN LATERAL jsonb_object_keys(COALESCE(u.permissions, '{}'::jsonb)) AS k
		WHERE u.email = 'unrelated@keluargamart.local' AND k LIKE 'system.users.%'`).Scan(&m); err != nil {
		t.Fatal(err)
	}
	if m != 1 {
		t.Errorf("the unrelated account's system.users.view override changed (%d found); "+
			"000105 must only touch the two accounts the ruling names", m)
	}
}

// hasKey reports whether a raw JSONB text contains an exact quoted top-level
// key. The keys this file checks for are dotted permission names, so the quotes
// are what make the match exact: a substring check on the bare key would also
// match a key named "sales.*.something", and the deny assertions depend on the
// difference.
func hasKey(raw, key string) bool {
	return strings.Contains(raw, `"`+key+`"`)
}

// hasDeny reports whether the override holds an explicit `false` on a key. The
// value is matched rather than the pair, because jsonb's text form is not
// guaranteed to put exactly one space after the colon.
func hasDeny(raw, key string) bool {
	return strings.Contains(raw, `"`+key+`"`) && !strings.Contains(raw, `"`+key+`": true`)
}
