package persistence

import (
	"database/sql"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestSessionRepositoryBearerSchemaReadinessOnTiDB(t *testing.T) {
	db, cleanup := openSessionRepositoryTiDB(t)
	defer cleanup()
	createCanonicalSystemTablesForIntegration(t, db, constants.TableSession)

	if err := VerifySessionBearerSchemaReadiness(t.Context(), db); err != nil {
		t.Fatalf("canonical session schema readiness: %v", err)
	}
	if _, err := db.Exec("ALTER TABLE `_System_Session` DROP COLUMN `CurrentBearerToken`"); err != nil {
		t.Fatal(err)
	}
	if err := VerifySessionBearerSchemaReadiness(t.Context(), db); err == nil || !strings.Contains(err.Error(), "missing or incompatible") {
		t.Fatalf("incompatible session schema readiness error = %v", err)
	}
	if _, err := db.Exec("ALTER TABLE `_System_Session` ADD COLUMN `CurrentBearerToken` VARCHAR(3000) NULL"); err != nil {
		t.Fatal(err)
	}
	if err := VerifySessionBearerSchemaReadiness(t.Context(), db); err == nil || !strings.Contains(err.Error(), "missing or incompatible") {
		t.Fatalf("wrong-type session schema readiness error = %v", err)
	}
}

func TestSessionRepositoryCASAndRevocationOutcomesOnTiDB(t *testing.T) {
	db, cleanup := openSessionRepositoryTiDB(t)
	defer cleanup()
	createCanonicalSystemTablesForIntegration(t, db, constants.TableSession)
	repo := NewSessionRepository(db)

	now := time.Now().UTC().Truncate(time.Second)
	proof := strings.Repeat("a", 64)
	if err := repo.InsertSession(t.Context(), &models.SystemSession{
		ID: "session-cas", UserID: "user-1", Token: "bearer-1", AuthEpoch: 1,
		ExpiresAt: now.Add(time.Hour), LastActivity: now, IPAddress: "127.0.0.1", UserAgent: "integration", ControlProofHash: &proof,
	}); err != nil {
		t.Fatalf("InsertSession: %v", err)
	}
	session, err := repo.GetSession(t.Context(), "session-cas")
	if err != nil || session == nil || session.Token != "bearer-1" || session.AuthEpoch != 1 || session.ControlProofHash == nil || *session.ControlProofHash != proof {
		t.Fatalf("GetSession = %#v, %v", session, err)
	}
	if err := repo.InsertSession(t.Context(), &models.SystemSession{
		ID: "session-max-epoch", UserID: "user-1", Token: "bearer-max", AuthEpoch: ^uint64(0),
		ExpiresAt: now.Add(time.Hour), LastActivity: now, IPAddress: "127.0.0.1", UserAgent: "integration",
	}); err != nil {
		t.Fatalf("InsertSession max epoch: %v", err)
	}
	maxEpochSession, err := repo.GetSession(t.Context(), "session-max-epoch")
	if err != nil || maxEpochSession == nil || maxEpochSession.AuthEpoch != ^uint64(0) {
		t.Fatalf("max-epoch GetSession = %#v, %v", maxEpochSession, err)
	}
	if err := repo.InsertSession(t.Context(), &models.SystemSession{
		ID: "session-renew", UserID: "user-1", Token: "renew-old",
		ExpiresAt: now.Add(time.Hour), LastActivity: now, IPAddress: "127.0.0.1", UserAgent: "integration",
	}); err != nil {
		t.Fatalf("InsertSession ordinary renewal: %v", err)
	}
	if renewed, err := repo.RenewSession(t.Context(), "session-renew", "renew-old", "renew-new", now.Add(2*time.Hour), now); err != nil || !renewed {
		t.Fatalf("RenewSession = %v, %v; want true, nil", renewed, err)
	}
	renewedSession, err := repo.GetSession(t.Context(), "session-renew")
	if err != nil || renewedSession == nil || renewedSession.Token != "renew-new" {
		t.Fatalf("renewed GetSession = %#v, %v", renewedSession, err)
	}

	if next, err := repo.AdvanceSessionAuthEpoch(t.Context(), "session-cas", 1); err != nil || next != 2 {
		t.Fatalf("AdvanceSessionAuthEpoch = %d, %v; want 2, nil", next, err)
	}
	if next, err := repo.AdvanceSessionAuthEpoch(t.Context(), "session-cas", 1); !errors.Is(err, ErrSessionAuthEpochConflict) || next != 0 {
		t.Fatalf("stale AdvanceSessionAuthEpoch = %d, %v", next, err)
	}

	if renewed, err := repo.RenewSessionWithControlProof(t.Context(), "session-cas", "bearer-1", "wrong-proof", "bearer-2", now.Add(2*time.Hour), now); err != nil || renewed {
		t.Fatalf("wrong-proof RenewSessionWithControlProof = %v, %v", renewed, err)
	}
	if renewed, err := repo.RenewSessionWithControlProof(t.Context(), "session-cas", "wrong-bearer", proof, "bearer-2", now.Add(2*time.Hour), now); err != nil || renewed {
		t.Fatalf("wrong-bearer RenewSessionWithControlProof = %v, %v", renewed, err)
	}
	if renewed, err := repo.RenewSessionWithControlProof(t.Context(), "session-cas", "bearer-1", proof, "bearer-2", now.Add(2*time.Hour), now); err != nil || !renewed {
		t.Fatalf("RenewSessionWithControlProof = %v, %v; want true, nil", renewed, err)
	}
	if revoked, err := repo.RevokeSession(t.Context(), "session-cas", "bearer-1"); err != nil || revoked {
		t.Fatalf("superseded bearer RevokeSession = %v, %v", revoked, err)
	}
	if revoked, err := repo.RevokeSessionWithControlProof(t.Context(), "session-cas", "wrong-proof"); err != nil || revoked {
		t.Fatalf("wrong-proof RevokeSessionWithControlProof = %v, %v", revoked, err)
	}
	if revoked, err := repo.RevokeSessionWithControlProof(t.Context(), "session-cas", proof); err != nil || !revoked {
		t.Fatalf("RevokeSessionWithControlProof = %v, %v; want true, nil", revoked, err)
	}
	if renewed, err := repo.RenewSessionWithControlProof(t.Context(), "session-cas", "bearer-2", proof, "bearer-3", now.Add(3*time.Hour), now); err != nil || renewed {
		t.Fatalf("revoked RenewSessionWithControlProof = %v, %v", renewed, err)
	}
	session, err = repo.GetSession(t.Context(), "session-cas")
	if err != nil || session == nil || session.Token != "bearer-2" || !session.IsRevoked || session.ControlProofHash != nil {
		t.Fatalf("revoked GetSession = %#v, %v", session, err)
	}
}

func TestSessionRepositoryBulkRevocationOutcomesOnTiDB(t *testing.T) {
	db, cleanup := openSessionRepositoryTiDB(t)
	defer cleanup()
	createCanonicalSystemTablesForIntegration(t, db, constants.TableSession)
	if _, err := db.Exec(`CREATE TABLE User (Id VARCHAR(255) PRIMARY KEY, ProfileId VARCHAR(255), UserRoleId VARCHAR(255)) ` + constants.DefaultTableOptions); err != nil {
		t.Fatal(err)
	}
	for _, statement := range []string{
		`INSERT INTO User (Id, ProfileId, UserRoleId) VALUES
			('user-profile', 'profile-target', 'role-other'),
			('user-role', 'profile-other', 'role-target'),
			('user-direct', 'profile-other', 'role-other'),
			('user-app', 'profile-other', 'role-other'),
			('user-combo', 'profile-other', 'role-other')`,
	} {
		if _, err := db.Exec(statement); err != nil {
			t.Fatal(err)
		}
	}
	repo := NewSessionRepository(db)
	now := time.Now().UTC().Truncate(time.Second)
	for _, session := range []struct{ id, user, app string }{
		{id: "session-profile", user: "user-profile", app: "app-other"},
		{id: "session-role", user: "user-role", app: "app-other"},
		{id: "session-direct", user: "user-direct", app: "app-other"},
		{id: "session-app", user: "user-app", app: "app-target"},
		{id: "session-combo", user: "user-combo", app: "app-combo"},
	} {
		app := session.app
		proof := strings.Repeat(session.id[len(session.id)-1:], 64)
		if err := repo.InsertSession(t.Context(), &models.SystemSession{
			ID: session.id, UserID: session.user, Token: "bearer-" + session.id,
			ConnectedAppID: &app, ControlProofHash: &proof,
			ExpiresAt: now.Add(time.Hour), LastActivity: now, IPAddress: "127.0.0.1", UserAgent: "integration",
		}); err != nil {
			t.Fatalf("InsertSession %s: %v", session.id, err)
		}
	}

	if rows, err := repo.RevokeAllSessionsByProfileID(t.Context(), "profile-target"); err != nil || rows != 1 {
		t.Fatalf("RevokeAllSessionsByProfileID = %d, %v", rows, err)
	}
	if rows, err := repo.RevokeAllSessionsByUserRoleID(t.Context(), "role-target"); err != nil || rows != 1 {
		t.Fatalf("RevokeAllSessionsByUserRoleID = %d, %v", rows, err)
	}
	if err := repo.RevokeAllSessionsForUser(t.Context(), "user-direct"); err != nil {
		t.Fatalf("RevokeAllSessionsForUser: %v", err)
	}
	if rows, err := repo.RevokeAllSessionsForConnectedApp(t.Context(), "app-target"); err != nil || rows != 1 {
		t.Fatalf("RevokeAllSessionsForConnectedApp = %d, %v", rows, err)
	}
	if rows, err := repo.RevokeSessionsForUserAndConnectedApp(t.Context(), "user-combo", "app-combo"); err != nil || rows != 1 {
		t.Fatalf("RevokeSessionsForUserAndConnectedApp = %d, %v", rows, err)
	}

	var revoked, proofs int
	if err := db.QueryRow(`SELECT SUM(IsRevoked), SUM(ControlProofHash IS NOT NULL) FROM _System_Session`).Scan(&revoked, &proofs); err != nil {
		t.Fatal(err)
	}
	if revoked != 5 || proofs != 0 {
		t.Fatalf("bulk revocation state = revoked:%d proof-bearing:%d, want 5/0", revoked, proofs)
	}
}

func TestSessionRepositoryConnectedAppVisibilityOutcomesOnTiDB(t *testing.T) {
	db, cleanup := openSessionRepositoryTiDB(t)
	defer cleanup()
	if _, err := db.Exec(`CREATE TABLE User (Id VARCHAR(255) PRIMARY KEY) ` + constants.DefaultTableOptions); err != nil {
		t.Fatal(err)
	}
	createCanonicalSystemTablesForIntegration(t, db,
		"_System_InstalledPack",
		constants.TableConnectedApp,
		"_System_ConnectedAppDeclaration",
		"_System_OAuthDynamicClientRegistration",
	)
	for _, statement := range []string{
		`INSERT INTO _System_InstalledPack (Id, Name, Category) VALUES
			('pack-crm', 'salesforce-crm-standard', 'application'),
			('pack-other', 'other-pack', 'application')`,
		`INSERT INTO _System_ConnectedApp (Id, Name, ConsumerKey, IsActive) VALUES
			('ownerless', 'Ownerless', 'key-ownerless', 1),
			('inactive', 'Inactive', 'key-inactive', 0),
			('source-backed', 'Source Backed', 'key-source', 1),
			('ambiguous', 'Ambiguous', 'key-ambiguous', 1)`,
		`INSERT INTO _System_ConnectedAppDeclaration (Id, InstalledPackName, ConnectedAppId, PayloadHash) VALUES
			('decl-source', 'salesforce-crm-standard', 'source-backed', 'hash-source'),
			('decl-ambiguous', 'salesforce-crm-standard', 'ambiguous', 'hash-ambiguous')`,
		`INSERT INTO _System_OAuthDynamicClientRegistration (Id, ConnectedAppId, ScopeOwnerPack, RegisteredByUserId) VALUES
			('reg-ambiguous', 'ambiguous', 'other-pack', 'user-1')`,
	} {
		if _, err := db.Exec(statement); err != nil {
			t.Fatalf("seed connected-app visibility fixture: %v\n%s", err, statement)
		}
	}

	for _, tc := range []struct {
		name, app string
		repo      *SessionRepository
		want      bool
	}{
		{name: "ownerless active", app: "ownerless", repo: NewSessionRepository(db), want: true},
		{name: "inactive", app: "inactive", repo: NewSessionRepository(db)},
		{name: "source backed without host policy", app: "source-backed", repo: NewSessionRepository(db)},
		{name: "ambiguous bindings", app: "ambiguous", repo: NewSessionRepository(db)},
		{name: "independent identity sees durable source app", app: "source-backed", repo: NewIndependentIdentitySessionRepository(db), want: true},
		{name: "independent identity still rejects inactive app", app: "inactive", repo: NewIndependentIdentitySessionRepository(db)},
		{name: "host policy allows owning pack", app: "source-backed", repo: sessionRepositoryWithVisibility(db, "salesforce-crm-standard"), want: true},
		{name: "host policy rejects other pack", app: "source-backed", repo: sessionRepositoryWithVisibility(db, "other-pack")},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got, err := tc.repo.IsConnectedAppActive(t.Context(), tc.app)
			if err != nil || got != tc.want {
				t.Fatalf("IsConnectedAppActive = %v, %v; want %v, nil", got, err, tc.want)
			}
		})
	}
}

func sessionRepositoryWithVisibility(db *sql.DB, ownerPack string) *SessionRepository {
	repo := NewSessionRepository(db)
	repo.SetConnectedAppRuntimeVisibilityPolicy(integrationSessionConnectedAppVisibilityPolicy{ownerPack: true})
	return repo
}

type integrationSessionConnectedAppVisibilityPolicy map[string]bool

func (p integrationSessionConnectedAppVisibilityPolicy) AllowsConnectedApp(ownerPack, _ string) bool {
	return p[strings.ToLower(strings.TrimSpace(ownerPack))]
}
