package repositories

import (
	"testing"

	"nearle/models"
)

// A PIN has to survive a round trip through a `bigint` column, and has to be
// hard enough to guess to be worth having. These cover both, because the schema
// makes the first one non-obvious.

func TestAPinMustSurviveTheColumnItIsStoredIn(t *testing.T) {
	// `app_users.pin` is a bigint. "0451" stored there comes back as 451, so a
	// cashier would type four digits and be refused for ever. Live data already
	// holds one such account.
	if _, err := validatePosPin("0451"); err == nil {
		t.Fatal("a PIN starting with zero was accepted; it cannot round-trip through a bigint")
	}

	value, err := validatePosPin("4821")
	if err != nil {
		t.Fatalf("a good PIN was refused: %v", err)
	}
	if value != 4821 {
		t.Fatalf("PIN parsed to %d, want 4821", value)
	}
}

func TestAPinIsExactlyFourDigits(t *testing.T) {
	for _, pin := range []string{"123", "12345", "abcd", "12a4", "  12 "} {
		if _, err := validatePosPin(pin); err == nil {
			t.Errorf("PIN %q was accepted", pin)
		}
	}
}

// The first thing anyone tries. Live data has 1234 on eleven accounts and 1111
// on nine, which is exactly the outcome this prevents repeating.
func TestAnObviousPinIsRefused(t *testing.T) {
	for _, pin := range []string{"1234", "1111", "2345", "4321", "9999", "2222"} {
		if _, err := validatePosPin(pin); err == nil {
			t.Errorf("PIN %q was accepted despite being one of the first guessed", pin)
		}
	}
}

// An empty PIN is not an error — somebody may be given a password instead. The
// caller decides whether having neither is a problem.
func TestAnAbsentPinIsNotAnError(t *testing.T) {
	value, err := validatePosPin("")
	if err != nil {
		t.Fatalf("an absent PIN was treated as invalid: %v", err)
	}
	if value != 0 {
		t.Fatalf("an absent PIN parsed to %d, want 0", value)
	}
}

// A stored PIN the schema cannot represent as four digits comes back empty
// rather than short, because a three-digit PIN on screen is one a cashier
// cannot type — and they would have no way to describe the fault.
func TestAnUnrepresentablePinIsNotShown(t *testing.T) {
	if got := posPinString(451); got != "" {
		t.Fatalf("a three-digit PIN rendered as %q, want empty", got)
	}
	if got := posPinString(0); got != "" {
		t.Fatalf("an unset PIN rendered as %q, want empty", got)
	}
	if got := posPinString(4821); got != "4821" {
		t.Fatalf("PIN rendered as %q, want 4821", got)
	}
}

func TestANameIsSplitAcrossTheTwoColumnsThisSchemaHas(t *testing.T) {
	cases := []struct {
		in          string
		first, last string
	}{
		{"Asha", "Asha", ""},
		{"Asha Kumar", "Asha", "Kumar"},
		{"Ragul Kannan Selvam", "Ragul", "Kannan Selvam"},
		{"   Divya   R  ", "Divya", "R"},
		{"", "", ""},
	}

	for _, tc := range cases {
		first, last := splitName(tc.in)
		if first != tc.first || last != tc.last {
			t.Errorf("splitName(%q) = (%q, %q), want (%q, %q)",
				tc.in, first, last, tc.first, tc.last)
		}
	}
}

// Only a role that can actually be checked should grant anything. Zero is the
// one that matters: it is not a role, it is what an account carries when nobody
// set one, and live data has riders and shop accounts sharing it.
func TestOnlyRealRolesCanManageStaff(t *testing.T) {
	if models.PosRoleCanManageStaff(0) {
		t.Error("roleid 0 was allowed to manage staff; it is unset, not a role")
	}
	if models.PosRoleCanManageStaff(models.PosRoleCashier) {
		t.Error("a cashier was allowed to manage staff, so could promote themselves")
	}
	if !models.PosRoleCanManageStaff(models.PosRoleSupervisor) {
		t.Error("a supervisor was refused staff management, which is their whole purpose")
	}
	// A Nearle Daily role is not a POS role. This once granted staff management
	// to 1 through 6, on the reasoning that a browser administrator loses
	// nothing by standing at the counter — which handed till-supervisor powers
	// to 68 live accounts, 59 of them platform Super admins, not one of them
	// anybody's POS administrator. The back office provisions a supervisor; it
	// does not become one.
	for _, role := range []int{1, 2, 3, 4, 5, 6} {
		if models.PosRoleCanManageStaff(role) {
			t.Errorf("back-office role %d was granted till staff management", role)
		}
	}
}

// The till and the Nearle Daily application share one table and nothing else.
// Eligibility is provisioned, never inherited.
func TestOnlyPosRolesCanOpenATill(t *testing.T) {
	for _, role := range []int{models.PosRoleSupervisor, models.PosRoleCashier} {
		if !models.PosRoleEligible(role) {
			t.Errorf("POS role %d was refused a till", role)
		}
	}
	// Zero matters most: it is not a role but the absence of one, and 22 live
	// accounts carry it, including a delivery rider.
	for _, role := range []int{0, 1, 2, 3, 4, 5, 6, 9, 99, -1} {
		if models.PosRoleEligible(role) {
			t.Errorf("non-POS role %d was allowed to open a till", role)
		}
	}
}

func TestARoleIsReadFromItsNameNotItsNumber(t *testing.T) {
	if got := models.PosRoleFromName("supervisor"); got != models.PosRoleSupervisor {
		t.Errorf("supervisor = %d, want %d", got, models.PosRoleSupervisor)
	}
	if got := models.PosRoleFromName("  Cashier "); got != models.PosRoleCashier {
		t.Errorf("cashier = %d, want %d", got, models.PosRoleCashier)
	}
	// Anything unrecognised is zero, and every caller treats zero as a refusal
	// rather than as a default — an unknown role must never become a supervisor.
	for _, name := range []string{"", "admin", "manager", "owner", "7"} {
		if got := models.PosRoleFromName(name); got != 0 {
			t.Errorf("PosRoleFromName(%q) = %d, want 0", name, got)
		}
	}
}

// The web console writes `app_users` too, through `tenants/createstaff`, and
// that path had no validation at all. These cover the shared rule set, so a
// person created from a browser is subject to the same constraints as one
// created at a till — two paths writing one table is how they drift.

func TestStaffFromTheWebConsoleObeysTheTillsRules(t *testing.T) {
	cases := []struct {
		name string
		user models.User
		ok   bool
	}{
		{
			name: "a usable cashier",
			user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 7391},
			ok:   true,
		},
		{
			name: "a password instead of a PIN is fine",
			user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Password: "s3cret"},
			ok:   true,
		},
		{
			name: "no name",
			user: models.User{Roleid: models.PosRoleCashier, Pin: 7391},
		},
		{
			name: "no role — 0 is unset, not a role",
			user: models.User{Firstname: "Asha", Pin: 7391},
		},
		{
			name: "no way at all to sign in",
			user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier},
		},
		{
			// 451 is what "0451" becomes in a bigint column. Accepting it here
			// creates somebody who types four digits and is refused for ever.
			name: "a PIN the column cannot hold",
			user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 451},
		},
		{
			name: "a PIN anyone would guess first",
			user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 1234},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			user := tc.user
			_, err := ValidateStaffUser(&user)

			if tc.ok && err != nil {
				t.Fatalf("refused a valid staff row: %v", err)
			}
			if !tc.ok && err == nil {
				t.Fatal("accepted a staff row the till could not use")
			}
		})
	}
}
