package webhooks

import (
	"bytes"
	"context"
	"errors"
	"regexp"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-connectors/features/namedcredential"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/contract"
)

// testCipher returns a deterministic AES-256-GCM cipher suitable for tests.
func testCipher(t *testing.T) *namedcredential.SecretCipher {
	t.Helper()
	c, err := namedcredential.NewSecretCipherFromKey(bytes.Repeat([]byte{0x42}, 32))
	if err != nil {
		t.Fatalf("testCipher: %v", err)
	}
	return c
}

// encryptForTest is a helper that encrypts a plaintext with testCipher, for
// populating sqlmock rows with ciphertext as the service would persist.
func encryptForTest(t *testing.T, plaintext string) string {
	t.Helper()
	ct, err := testCipher(t).Encrypt(plaintext)
	if err != nil {
		t.Fatalf("encryptForTest(%q): %v", plaintext, err)
	}
	return ct
}

func TestWebhookSecretAdmin_ListReturnsMasked(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	ciphertext := encryptForTest(t, "supersecretvalue")
	mock.ExpectQuery(regexp.QuoteMeta("SELECT ws.Id, ws.OwnerPack, ws.Name, ws.Algorithm, ws.Description")).
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "OwnerPack", "Name", "Algorithm", "Description", "SecretValue", "RotatedAt",
		}).AddRow(
			"def-001", "my-pack", "order-webhook", "HMAC-SHA256", nil, ciphertext, nil,
		))

	items, err := NewWebhookSecretAdminService(db, testCipher(t)).ListConnectorWebhookSecrets(context.Background(), "")
	if err != nil {
		t.Fatalf("ListConnectorWebhookSecrets: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("want 1 item, got %d", len(items))
	}
	if items[0].MaskedHint != "whsec_•••alue" {
		t.Errorf("MaskedHint = %q, want %q", items[0].MaskedHint, "whsec_•••alue")
	}
	if strings.Contains(items[0].MaskedHint, "supersecretvalue") {
		t.Errorf("masked hint contains raw SecretValue; must not")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestWebhookSecretAdmin_RotateSuccess(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(regexp.QuoteMeta("SELECT 1 FROM _System_WebhookSecret")).
		WithArgs("def-001").
		WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO _System_WebhookSecretValue")).
		WillReturnResult(sqlmock.NewResult(1, 1))

	result, err := NewWebhookSecretAdminService(db, testCipher(t)).RotateConnectorWebhookSecret(
		context.Background(),
		"def-001",
		"newvalue-long-enough-secret",
	)
	if err != nil {
		t.Fatalf("RotateConnectorWebhookSecret: %v", err)
	}
	if result.ID != "def-001" {
		t.Fatalf("result.ID = %q, want def-001", result.ID)
	}
	if result.MaskedHint != "whsec_•••cret" {
		t.Fatalf("MaskedHint = %q, want whsec_•••cret", result.MaskedHint)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestWebhookSecretAdmin_RotateThenVerify_ReturnsNewSecret(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	cipher := testCipher(t)

	const (
		packNamespace = "my-connector-pack"
		secretName    = "order-events"
		defID         = "def-001"
		secretV1      = "v1-secret-value-long-enough"
	)

	mock.ExpectQuery(regexp.QuoteMeta("SELECT 1 FROM _System_WebhookSecret")).
		WithArgs(defID).
		WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO _System_WebhookSecretValue")).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if _, err := NewWebhookSecretAdminService(db, cipher).RotateConnectorWebhookSecret(context.Background(), defID, secretV1); err != nil {
		t.Fatalf("RotateConnectorWebhookSecret: %v", err)
	}

	storedCiphertext := encryptForTest(t, secretV1)
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Id, PackInstallId, Name, Category, Status")).
		WithArgs(packNamespace, "connector-pack", "active").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "PackInstallId", "Name", "Category", "Status", "AppliedPayloadHash", "PackVersion"}).
			AddRow("pack-id", "install-id-v1", packNamespace, "connector-pack", "active", "payload-hash-1", "1.0.0"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Id, Algorithm")).
		WithArgs(packNamespace, secretName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Algorithm"}).
			AddRow(defID, "HMAC-SHA256"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT SecretValue")).
		WithArgs(defID).
		WillReturnRows(sqlmock.NewRows([]string{"SecretValue"}).AddRow(storedCiphertext))

	resolved, err := ResolveWebhookSecretForVerification(context.Background(), db, cipher, packNamespace, secretName, "")
	if err != nil {
		t.Fatalf("ResolveWebhookSecretForVerification: %v", err)
	}
	if string(resolved.Secret) != secretV1 {
		t.Errorf("resolved.Secret = %q, want %q", resolved.Secret, secretV1)
	}

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestWebhookSecretAdmin_RotateRejectsShortSecret(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	_, err = NewWebhookSecretAdminService(db, testCipher(t)).RotateConnectorWebhookSecret(context.Background(), "def-001", "tooshort")
	var secretErr *contract.ConnectorWebhookSecretsError
	if !errors.As(err, &secretErr) {
		t.Fatalf("err = %T, want ConnectorWebhookSecretsError", err)
	}
	if secretErr.Kind != contract.ConnectorWebhookSecretsErrorSecretTooShort {
		t.Fatalf("kind = %s, want %s", secretErr.Kind, contract.ConnectorWebhookSecretsErrorSecretTooShort)
	}
	if secretErr.MinLength != 16 {
		t.Fatalf("MinLength = %d, want 16", secretErr.MinLength)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestRotateSecret_RequiresCipher(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	_, err = NewWebhookSecretAdminService(db, nil).RotateConnectorWebhookSecret(context.Background(), "def-001", "newvalue-long-enough-secret")
	var secretErr *contract.ConnectorWebhookSecretsError
	if !errors.As(err, &secretErr) {
		t.Fatalf("err = %T, want ConnectorWebhookSecretsError", err)
	}
	if secretErr.Kind != contract.ConnectorWebhookSecretsErrorEncryptionUnconfigured {
		t.Fatalf("kind = %s, want %s", secretErr.Kind, contract.ConnectorWebhookSecretsErrorEncryptionUnconfigured)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}
