package utils

import (
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"
	"time"
)

func withSecret(t *testing.T, secret string) {
	t.Helper()
	t.Setenv("POS_TOKEN_SECRET", secret)
}

const testSecret = "a-test-signing-key-long-enough"

func TestASessionSurvivesTheRoundTrip(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC)

	token, expires, err := MintPosToken(PosClaims{
		Userid: 42, Tenantid: 1087, Locationid: 1135, Roleid: 3, Terminalid: "T5EDD",
	}, now)
	if err != nil {
		t.Fatalf("minting: %v", err)
	}

	claims, err := ParsePosToken(token, now.Add(time.Hour))
	if err != nil {
		t.Fatalf("parsing a token we just issued: %v", err)
	}

	if claims.Tenantid != 1087 || claims.Locationid != 1135 {
		t.Fatalf("the outlet did not survive: tenant %d location %d", claims.Tenantid, claims.Locationid)
	}
	if claims.Terminalid != "T5EDD" {
		t.Fatalf("terminal id lost: %q", claims.Terminalid)
	}
	if !expires.After(now) {
		t.Fatalf("expiry %v is not after issue %v", expires, now)
	}
}

// The whole point of signing. Before this existed a till named its own outlet
// on the wire and was believed, so this is the test that says it no longer can.
func TestARewrittenOutletIsRefused(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC)

	token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now)
	if err != nil {
		t.Fatalf("minting: %v", err)
	}

	// Tamper: decode the payload, move it to another tenant's outlet, re-encode
	// and keep the original signature — exactly what an attacker holding a real
	// token would try.
	encoded, signature, _ := strings.Cut(token, ".")
	payload, err := base64.RawURLEncoding.DecodeString(encoded)
	if err != nil {
		t.Fatalf("decoding our own payload: %v", err)
	}

	var claims PosClaims
	if err := json.Unmarshal(payload, &claims); err != nil {
		t.Fatalf("unmarshalling our own payload: %v", err)
	}
	claims.Tenantid = 916
	claims.Locationid = 1185

	forged, _ := json.Marshal(claims)
	tampered := base64.RawURLEncoding.EncodeToString(forged) + "." + signature

	if _, err := ParsePosToken(tampered, now); err == nil {
		t.Fatal("a token whose outlet was rewritten was accepted")
	}
}

func TestAnExpiredSessionIsRefused(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC)

	token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now)
	if err != nil {
		t.Fatalf("minting: %v", err)
	}

	if _, err := ParsePosToken(token, now.Add(PosTokenTTL+time.Minute)); err == nil {
		t.Fatal("an expired session was accepted")
	}
}

// A token signed by somebody else must not verify here, or the signature is
// decoration.
func TestATokenFromAnotherKeyIsRefused(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC)

	token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 1087, Locationid: 1135}, now)
	if err != nil {
		t.Fatalf("minting: %v", err)
	}

	withSecret(t, "a-completely-different-key-here")
	if _, err := ParsePosToken(token, now); err == nil {
		t.Fatal("a token signed with another key verified")
	}
}

func TestAMalformedTokenIsRefused(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Now()

	for _, token := range []string{
		"",
		"nodot",
		".",
		"only.",
		".onlysignature",
		"not-base64!.also-not-base64!",
	} {
		if _, err := ParsePosToken(token, now); err == nil {
			t.Fatalf("malformed token %q was accepted", token)
		}
	}
}

// A deployment with no signing key must fail loudly rather than fall back to a
// key anyone reading the source could compute.
func TestNoSecretMeansNoSessions(t *testing.T) {
	t.Setenv("POS_TOKEN_SECRET", "")
	t.Setenv("JWT_SECRET_KEY", "")

	if PosTokenConfigured() {
		t.Fatal("reported configured with no secret set")
	}
	if _, _, err := MintPosToken(PosClaims{Tenantid: 1, Locationid: 1}, time.Now()); err == nil {
		t.Fatal("minted a session with no signing key")
	}
}

func TestAShortSecretIsRefused(t *testing.T) {
	t.Setenv("POS_TOKEN_SECRET", "short")
	t.Setenv("JWT_SECRET_KEY", "")

	if _, _, err := MintPosToken(PosClaims{Tenantid: 1, Locationid: 1}, time.Now()); err == nil {
		t.Fatal("signed with a secret too short to be worth signing with")
	}
}

// A token that verifies but names no outlet authorises nothing, and must not be
// mistaken for one that authorises everything.
func TestASessionNamingNoOutletIsRefused(t *testing.T) {
	withSecret(t, testSecret)
	now := time.Date(2026, 8, 6, 10, 0, 0, 0, time.UTC)

	token, _, err := MintPosToken(PosClaims{Userid: 1, Tenantid: 0, Locationid: 0}, now)
	if err != nil {
		t.Fatalf("minting: %v", err)
	}

	if _, err := ParsePosToken(token, now); err == nil {
		t.Fatal("a session naming no outlet was accepted")
	}
}
