package persistence

import (
	"context"
	"database/sql"
	"fmt"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
)

// TestIntegration_AuthorizationGrantLifecycleOnTiDB exercises the generic grant
// adapter against TiDB, including the actual caller-owned lock sequence. The
// fixture's authorization_clients table represents a product-owned client
// record: L0 does not know that table or how the product locks it.
func TestIntegration_AuthorizationGrantLifecycleOnTiDB(t *testing.T) {
	db, cleanup := openSchemaDDLTiDB(t)
	defer cleanup()

	for _, statement := range []string{
		"CREATE TABLE `User` (Id VARCHAR(64) PRIMARY KEY)",
		`CREATE TABLE authorization_clients (Id VARCHAR(64) PRIMARY KEY)`,
		`CREATE TABLE _System_OAuthConsent (
			Id VARCHAR(64) PRIMARY KEY,
			UserId VARCHAR(64) NOT NULL,
			ConnectedAppId VARCHAR(64) NOT NULL,
			Scope TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
			ConsentVersion INT NOT NULL,
			ApprovedAt DATETIME(6) NOT NULL,
			RevokedAt DATETIME(6) NULL,
			RevokedBy VARCHAR(255) NULL,
			CreatedDate DATETIME NOT NULL,
			LastModifiedDate DATETIME NOT NULL,
			INDEX grant_lookup (UserId, ConnectedAppId, ConsentVersion)
		) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		"INSERT INTO `User` (Id) VALUES ('subject-1')",
		`INSERT INTO authorization_clients (Id) VALUES ('client-1')`,
	} {
		if _, err := db.Exec(statement); err != nil {
			t.Fatalf("prepare authorization-grant fixture: %v (%s)", err, statement)
		}
	}

	repo := NewOAuthRepository(db)
	ensure := func(version int, scope string) ports.AuthorizationGrant {
		t.Helper()
		var grant ports.AuthorizationGrant
		withAuthorizationClientLockOnTiDB(t, db, "client-1", func(ctx context.Context) error {
			var err error
			grant, err = repo.EnsureAuthorizationGrant(ctx, ports.AuthorizationGrantRequest{
				SubjectID: "subject-1", ClientID: "client-1", Scope: scope, PolicyVersion: version,
			})
			return err
		})
		return grant
	}

	era1 := ensure(1, "records.read")
	if era1.ID == "" {
		t.Fatal("first authorization grant has no immutable id")
	}

	// Policy version moves forward and then back. The version-1 row must not
	// revive when the numeric version returns to 1; each transition opens a new
	// immutable era and retires every previous live row.
	era2 := ensure(2, "records.read")
	if era2.ID == era1.ID {
		t.Fatal("policy-version change reused prior authorization era")
	}
	era3 := ensure(1, "records.read")
	if era3.ID == era1.ID || era3.ID == era2.ID {
		t.Fatalf("version rollback revived an old era: era1=%s era2=%s era3=%s", era1.ID, era2.ID, era3.ID)
	}

	// Scope can change while the policy version remains stable. A later return
	// to the old scope must open another era rather than revive era3.
	era4 := ensure(1, "records.write")
	era5 := ensure(1, "records.read")
	if era4.ID == era3.ID || era5.ID == era3.ID || era5.ID == era4.ID {
		t.Fatalf("scope rollback revived a prior authorization era: era3=%s era4=%s era5=%s", era3.ID, era4.ID, era5.ID)
	}

	// Production tables use utf8mb4_unicode_ci, but OAuth scope tokens remain
	// opaque and case-sensitive. A case-only policy change must retire the old
	// era instead of being absorbed by the table collation.
	era6 := ensure(1, "Records.Read")
	era7 := ensure(1, "records.read")
	if era6.ID == era5.ID || era7.ID == era5.ID || era7.ID == era6.ID {
		t.Fatalf("case-only scope rollback revived a prior authorization era: era5=%s era6=%s era7=%s", era5.ID, era6.ID, era7.ID)
	}

	withAuthorizationClientLockOnTiDB(t, db, "client-1", func(ctx context.Context) error {
		active, err := repo.AuthorizationGrantActive(ctx, era7.ID, "subject-1", "client-1", 1)
		if err != nil {
			return err
		}
		if !active {
			return fmt.Errorf("newest authorization era is not active")
		}
		active, err = repo.AuthorizationGrantActive(ctx, era5.ID, "subject-1", "client-1", 1)
		if err != nil {
			return err
		}
		if active {
			return fmt.Errorf("original authorization era revived after version rollback")
		}
		return nil
	})

	withAuthorizationClientLockOnTiDB(t, db, "client-1", func(ctx context.Context) error {
		revoked, err := repo.RevokeAuthorizationGrantsForSubjectAndClient(ctx, "subject-1", "client-1", "test:disconnect")
		if err != nil {
			return err
		}
		if revoked != 1 {
			return fmt.Errorf("subject/client revoke = %d, want 1 live era", revoked)
		}
		return nil
	})

	withAuthorizationClientLockOnTiDB(t, db, "client-1", func(ctx context.Context) error {
		active, err := repo.AuthorizationGrantActive(ctx, era7.ID, "subject-1", "client-1", 1)
		if err != nil {
			return err
		}
		if active {
			return fmt.Errorf("revoked authorization era is still active")
		}
		return nil
	})
}

func withAuthorizationClientLockOnTiDB(t *testing.T, db *sql.DB, clientID string, fn func(context.Context) error) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	tx, cleanup, err := BeginPessimisticTx(ctx, db)
	if err != nil {
		t.Fatalf("begin pessimistic authorization transaction: %v", err)
	}
	finished := false
	defer func() {
		if !finished {
			_ = tx.Rollback()
			_ = cleanup()
		}
	}()

	var locked string
	if err := tx.QueryRowContext(ctx, `SELECT Id FROM authorization_clients WHERE Id = ? FOR UPDATE`, clientID).Scan(&locked); err != nil {
		t.Fatalf("lock product client: %v", err)
	}
	txCtx := context.WithValue(ctx, txContextKey{}, tx)
	txCtx = context.WithValue(txCtx, pessimisticTxMarkerKey{}, true)
	txCtx = ports.WithAuthorizationGrantTransactionScope(txCtx)
	txCtx = ports.WithAuthorizationClientLock(txCtx, clientID)
	if err := fn(txCtx); err != nil {
		t.Fatalf("authorization grant operation: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("commit authorization transaction: %v", err)
	}
	if err := cleanup(); err != nil {
		t.Fatalf("cleanup authorization transaction: %v", err)
	}
	finished = true
}
