package auth

import (
	"context"
	"reflect"
	"strings"
	"testing"

	pkgauth "github.com/nexuscrm/kernel/pkg/auth"
	"github.com/nexuscrm/shared/pkg/models"
)

const establishTestPassword = "correct horse battery staple"

func establishmentService(t *testing.T) (*AuthService, *sessionRepoStub) {
	t.Helper()
	passwordHash, err := pkgauth.HashPassword(establishTestPassword)
	if err != nil {
		t.Fatalf("HashPassword: %v", err)
	}
	sessions := &sessionRepoStub{}
	users := &userRepoStub{
		user: &models.User{
			ID: "user-1", Username: "user@example.test", Email: "user@example.test",
			ProfileID: "profile-1", IsActive: true,
		},
		passwordHash: passwordHash,
	}
	return NewAuthService(users, sessions, nil), sessions
}

func establish(t *testing.T, svc *AuthService, password string) (*BrowserSessionEstablishment, error) {
	t.Helper()
	return svc.EstablishBrowserSession(context.Background(), "user@example.test", password, "127.0.0.1", "browser")
}

// TestEstablishBrowserSessionIssuesAProofBoundToTheStoredSession is the core behaviour:
// the caller gets a raw proof and persistence gets only its verifier, so the two are
// bound without the store ever holding the secret.
func TestEstablishBrowserSessionIssuesAProofBoundToTheStoredSession(t *testing.T) {
	svc, sessions := establishmentService(t)

	established, err := establish(t, svc, establishTestPassword)
	if err != nil {
		t.Fatalf("EstablishBrowserSession: %v", err)
	}
	if established.ControlProof == "" {
		t.Fatal("no control proof was issued, so the browser would have nothing to send")
	}
	if sessions.inserted == nil || sessions.inserted.ControlProofHash == nil {
		t.Fatalf("no controlled session was persisted: %#v", sessions.inserted)
	}
	got, ok := matchingSessionControlProofHash(established.ControlProof, sessions.inserted.ControlProofHash)
	if !ok || got != *sessions.inserted.ControlProofHash {
		t.Fatalf("persisted verifier does not match the delivered proof: %q, %v", got, ok)
	}
	if established.User.ID != "user-1" || established.ExpiresAt.IsZero() {
		t.Fatalf("establishment dropped session facts: %+v", established)
	}
}

// TestBrowserSessionEstablishmentCannotCarryABearer is the structural guarantee, and it
// is why this returns its own type instead of LoginResult. Login always mints a bearer;
// a result type with nowhere to put one means a handler cannot hand the page a readable
// credential even by accident.
//
// The field set is asserted rather than searched for known-bad names, so a field added
// later fails here instead of quietly becoming serializable.
func TestBrowserSessionEstablishmentCannotCarryABearer(t *testing.T) {
	fields := map[string]struct{}{}
	structType := reflect.TypeOf(BrowserSessionEstablishment{})
	for i := 0; i < structType.NumField(); i++ {
		fields[structType.Field(i).Name] = struct{}{}
	}

	want := map[string]struct{}{"ControlProof": {}, "ExpiresAt": {}, "User": {}}
	if !reflect.DeepEqual(fields, want) {
		t.Fatalf("field set = %v, want %v — a new field on this type is a new way for a credential to reach the page", fields, want)
	}

	// The proof is a secret too: it belongs in the cookie, never in a body.
	field, _ := structType.FieldByName("ControlProof")
	if field.Tag.Get("json") != "-" {
		t.Fatalf("ControlProof json tag = %q, want \"-\" so it cannot be marshalled into a response", field.Tag.Get("json"))
	}
}

// TestEstablishedSessionCarriesNoProgrammaticAuthority pins what the *stored row* must
// not contain. ValidateBrowserSession copies Scope, GrantType and ConnectedAppID out of
// the row into every request the browser makes on this session, so a value set at
// establishment silently becomes standing authority.
//
// The session class is deliberately not asserted here: _System_Session has no such
// column, so it never survives establishment. Asserting it would have been a test of a
// field that does not exist — which is how the doc comment claiming this session
// "declares what it is" got corrected.
func TestEstablishedSessionCarriesNoProgrammaticAuthority(t *testing.T) {
	svc, sessions := establishmentService(t)

	if _, err := establish(t, svc, establishTestPassword); err != nil {
		t.Fatalf("EstablishBrowserSession: %v", err)
	}
	if sessions.inserted == nil {
		t.Fatal("no session was persisted")
	}
	for name, value := range map[string]*string{
		"Scope":          sessions.inserted.Scope,
		"GrantType":      sessions.inserted.GrantType,
		"ConnectedAppID": sessions.inserted.ConnectedAppID,
	} {
		if value != nil && *value != "" {
			t.Fatalf("stored %s = %q: ValidateBrowserSession copies this into every browser request, making it standing authority", name, *value)
		}
	}
}

// TestEstablishBrowserSessionRejectsBadCredentials keeps establishment from becoming an
// account oracle at this layer, and confirms nothing is persisted on failure.
func TestEstablishBrowserSessionRejectsBadCredentials(t *testing.T) {
	svc, sessions := establishmentService(t)

	established, err := establish(t, svc, "wrong password")
	if err == nil || established != nil {
		t.Fatalf("EstablishBrowserSession = %#v, %v; want a failure", established, err)
	}
	if sessions.inserted != nil {
		t.Fatalf("a failed login persisted a session: %#v", sessions.inserted)
	}
	if strings.Contains(strings.ToLower(err.Error()), "password") &&
		!strings.Contains(strings.ToLower(err.Error()), "invalid email or password") {
		t.Fatalf("error distinguishes why the login failed: %v", err)
	}
}

// TestEstablishBrowserSessionRejectsANilService covers the receiver guard: an
// unconfigured service must not report a successful establishment.
func TestEstablishBrowserSessionRejectsANilService(t *testing.T) {
	var svc *AuthService
	established, err := establish(t, svc, establishTestPassword)
	if err == nil || established != nil {
		t.Fatalf("EstablishBrowserSession = %#v, %v; want a failure", established, err)
	}
}
