package namedcredential

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

func testProvisioningCipher(t *testing.T) *SecretCipher {
	t.Helper()
	cipher, err := NewSecretCipherFromKey(bytes.Repeat([]byte{0x42}, 32))
	if err != nil {
		t.Fatalf("test cipher: %v", err)
	}
	return cipher
}

func TestProvisionDefinitionAndSecretOnTiDB(t *testing.T) {
	db, cleanup := openNamedCredentialTiDB(t)
	defer cleanup()
	createNamedCredentialTables(t, db)

	ctx := context.Background()
	svc := NewService(NewRepository(db, testProvisioningCipher(t)), &mockPermChecker{allow: true})
	user := adminUser()
	def, err := svc.UpsertDefinition(ctx, user, Definition{
		DeveloperName: "ProvisionedNC",
		Label:         "Provisioned Named Credential",
		Endpoint:      "https://api.example.com",
		PrincipalType: PrincipalTypeNamedPrincipal,
		AuthType:      AuthTypeBearerToken,
	})
	if err != nil {
		t.Fatalf("UpsertDefinition: %v", err)
	}

	firstPlaintext := "server-internal-plaintext"
	firstMeta, err := svc.SetSecret(ctx, user, def.DeveloperName, "Token", firstPlaintext)
	if err != nil {
		t.Fatalf("SetSecret: %v", err)
	}
	if firstMeta.MaskedHint != "…text" {
		t.Fatalf("SetSecret maskedHint = %q, want %q", firstMeta.MaskedHint, "…text")
	}
	assertPersistedSecretIsEncrypted(t, db, def.ID, firstPlaintext)
	if got, err := svc.ResolveCredentialValue(ctx, def.DeveloperName, "Token"); err != nil || got != firstPlaintext {
		t.Fatalf("ResolveCredentialValue = (%q, %v), want first plaintext", got, err)
	}

	secondPlaintext := "rotated-server-secret"
	secondMeta, err := svc.SetSecret(ctx, user, def.DeveloperName, "Token", secondPlaintext)
	if err != nil {
		t.Fatalf("rotate secret: %v", err)
	}
	if secondMeta.MaskedHint != "…cret" {
		t.Fatalf("rotated maskedHint = %q, want %q", secondMeta.MaskedHint, "…cret")
	}
	assertPersistedSecretIsEncrypted(t, db, def.ID, secondPlaintext)
	if got, err := svc.ResolveCredentialValue(ctx, def.DeveloperName, "Token"); err != nil || got != secondPlaintext {
		t.Fatalf("ResolveCredentialValue after rotation = (%q, %v), want rotated plaintext", got, err)
	}

	var secretRows int
	if err := db.QueryRow(`SELECT COUNT(*) FROM _System_NamedCredentialSecret WHERE NamedCredentialId = ?`, def.ID).Scan(&secretRows); err != nil {
		t.Fatalf("count rotated secrets: %v", err)
	}
	if secretRows != 1 {
		t.Fatalf("secret rows after rotation = %d, want 1", secretRows)
	}

	if err := svc.DeleteDefinition(ctx, user, def.DeveloperName); err != nil {
		t.Fatalf("DeleteDefinition: %v", err)
	}
	if err := db.QueryRow(`SELECT COUNT(*) FROM _System_NamedCredentialSecret WHERE NamedCredentialId = ?`, def.ID).Scan(&secretRows); err != nil {
		t.Fatalf("count secrets after definition delete: %v", err)
	}
	if secretRows != 0 {
		t.Fatalf("secret rows after definition delete = %d, want cascade delete", secretRows)
	}
}

func assertPersistedSecretIsEncrypted(t *testing.T, db *sql.DB, definitionID, plaintext string) {
	t.Helper()
	var stored string
	if err := db.QueryRow(
		`SELECT SecretValue FROM _System_NamedCredentialSecret WHERE NamedCredentialId = ? AND SecretKind = 'Token'`,
		definitionID,
	).Scan(&stored); err != nil {
		t.Fatalf("read persisted secret: %v", err)
	}
	if stored == plaintext || strings.Contains(stored, plaintext) {
		t.Fatal("persisted SecretValue contains plaintext")
	}
}

func createNamedCredentialTables(t *testing.T, db *sql.DB) {
	t.Helper()
	for _, ddl := range []string{
		`CREATE TABLE _System_NamedCredential (
			Id VARCHAR(255) PRIMARY KEY,
			DeveloperName VARCHAR(255) NOT NULL UNIQUE,
			Label VARCHAR(255) NOT NULL,
			Endpoint TEXT NOT NULL,
			PrincipalType VARCHAR(32) NOT NULL,
			AuthType VARCHAR(64) NOT NULL,
			Description TEXT,
			OwnerPack VARCHAR(255),
			CreatedById VARCHAR(255),
			CreatedDate DATETIME DEFAULT CURRENT_TIMESTAMP,
			LastModifiedDate DATETIME DEFAULT CURRENT_TIMESTAMP
		)`,
		`CREATE TABLE _System_NamedCredentialSecret (
			Id VARCHAR(255) PRIMARY KEY,
			NamedCredentialId VARCHAR(255) NOT NULL,
			SecretKind VARCHAR(64) NOT NULL,
			SecretValue TEXT NOT NULL,
			MaskedHint VARCHAR(64),
			RotatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
			CreatedDate DATETIME DEFAULT CURRENT_TIMESTAMP,
			LastModifiedDate DATETIME DEFAULT CURRENT_TIMESTAMP,
			UNIQUE KEY ux_namedcred_secret_kind (NamedCredentialId, SecretKind),
			FOREIGN KEY (NamedCredentialId) REFERENCES _System_NamedCredential(Id) ON DELETE CASCADE
		)`,
	} {
		if _, err := db.Exec(ddl); err != nil {
			t.Fatalf("create named credential table: %v", err)
		}
	}
}

func openNamedCredentialTiDB(t *testing.T) (*sql.DB, func()) {
	t.Helper()
	host := strings.TrimSpace(os.Getenv("TIDB_HOST"))
	if host == "" {
		host = "127.0.0.1"
	}
	port := strings.TrimSpace(os.Getenv("TIDB_PORT"))
	if port == "" {
		port = "4001"
	}
	user := strings.TrimSpace(os.Getenv("TIDB_USER"))
	if user == "" {
		user = "root"
	}
	tlsParam := ""
	switch strings.ToLower(strings.TrimSpace(os.Getenv("TIDB_TLS"))) {
	case "1", "true", "yes", "require", "verify-full":
		tlsParam = "&tls=true"
	}
	adminDSN := fmt.Sprintf("%s:%s@tcp(%s:%s)/?parseTime=true&loc=Local%s", user, os.Getenv("TIDB_PASSWORD"), host, port, tlsParam)
	adminDB, err := sql.Open("mysql", adminDSN)
	if err != nil {
		unavailableNamedCredentialTiDB(t, "open admin TiDB: %v", err)
	}
	if err := adminDB.Ping(); err != nil {
		_ = adminDB.Close()
		unavailableNamedCredentialTiDB(t, "TiDB not reachable at %s:%s: %v", host, port, err)
	}
	dbName := fmt.Sprintf("nexuscrm_namedcredential_%d", time.Now().UnixNano())
	if _, err := adminDB.Exec("CREATE DATABASE `" + dbName + "`"); err != nil {
		_ = adminDB.Close()
		unavailableNamedCredentialTiDB(t, "create temporary database: %v", err)
	}
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&loc=Local%s", user, os.Getenv("TIDB_PASSWORD"), host, port, dbName, tlsParam)
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		_, _ = adminDB.Exec("DROP DATABASE IF EXISTS `" + dbName + "`")
		_ = adminDB.Close()
		t.Fatalf("open named credential TiDB: %v", err)
	}
	cleanup := func() {
		_ = db.Close()
		_, _ = adminDB.Exec("DROP DATABASE IF EXISTS `" + dbName + "`")
		_ = adminDB.Close()
	}
	return db, cleanup
}

func unavailableNamedCredentialTiDB(t *testing.T, format string, args ...any) {
	t.Helper()
	if os.Getenv("NEXUSCRM_TIDB_REQUIRED") == "1" {
		t.Fatalf("required TiDB integration unavailable: "+format, args...)
	}
	t.Skipf("skipping integration test: "+format, args...)
}
