package connector_health

import (
	"context"
	"errors"
	"regexp"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

type fakeCredentialResolver struct {
	validByRef map[string]bool
	err        error
}

func (f *fakeCredentialResolver) HasRequiredSecrets(_ context.Context, devName string) (bool, error) {
	if f.err != nil {
		return false, f.err
	}
	return f.validByRef[devName], nil
}

func expectActiveProfileRead(mock sqlmock.Sqlmock, namespace, installID string, rows *sqlmock.Rows) {
	mock.ExpectQuery(regexp.QuoteMeta("SELECT PackInstallId")).
		WithArgs(namespace, "active").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow(installID))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Status, COALESCE(AppliedPayloadHash, ''), COALESCE(DesiredPayloadHash, '')")).
		WithArgs(namespace, installID).
		WillReturnRows(sqlmock.NewRows([]string{"Status", "AppliedPayloadHash", "DesiredPayloadHash"}).AddRow("active", "hash", "hash"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(*)")).
		WithArgs(namespace, installID).
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT c.InstalledPackId, c.OwnerPack, c.ProfileName, c.CredentialRef, c.EndpointClass,")).
		WithArgs(namespace, installID).
		WillReturnRows(rows)
}

func profileRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"InstalledPackId",
		"OwnerPack",
		"ProfileName",
		"CredentialRef",
		"EndpointClass",
		"RetryPolicy",
		"PageSpec",
		"BatchSpec",
		"ObjectPathMap",
		"Description",
	})
}

func addProfileRow(rows *sqlmock.Rows, installID, ownerPack, profileName, credentialRef, batchSpec string) *sqlmock.Rows {
	return rows.AddRow(
		installID,
		ownerPack,
		profileName,
		credentialRef,
		"sync",
		`{"MaxAttempts":1}`,
		`{}`,
		batchSpec,
		`{}`,
		"",
	)
}

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

	expectActiveProfileRead(mock, "pack", "install-active", profileRows())

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if ok {
		t.Fatalf("CredentialValid = true with no callout profile")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

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

	rows := profileRows()
	addProfileRow(rows, "install-active", "pack", "a", "valid-cred", "{}")
	addProfileRow(rows, "install-active", "pack", "b", "missing-secret", "{}")
	expectActiveProfileRead(mock, "pack", "install-active", rows)

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{
		validByRef: map[string]bool{
			"valid-cred": true,
		},
	}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if ok {
		t.Fatalf("CredentialValid = true when a referenced credential is invalid")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

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

	rows := profileRows()
	addProfileRow(rows, "install-active", "pack", "a", "cred-a", "{}")
	addProfileRow(rows, "install-active", "pack", "b", "cred-b", "{}")
	expectActiveProfileRead(mock, "pack", "install-active", rows)

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{
		validByRef: map[string]bool{
			"cred-a": true,
			"cred-b": true,
		},
	}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if !ok {
		t.Fatalf("CredentialValid = false, want true")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

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

	rows := profileRows()
	addProfileRow(rows, "install-active", "pack", "a", "cred-a", "{}")
	expectActiveProfileRead(mock, "pack", "install-active", rows)

	_, err = NewCredentialStatusStore(db, &fakeCredentialResolver{err: errors.New("boom")}).
		CredentialValid(context.Background(), "pack")
	if err == nil {
		t.Fatalf("CredentialValid error = nil")
	}
}

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

	rows := profileRows()
	addProfileRow(rows, "install-active", "pack", "a", "profile-cred", `{"CredentialRef":"batch-cred"}`)
	expectActiveProfileRead(mock, "pack", "install-active", rows)

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{
		validByRef: map[string]bool{
			"profile-cred": true,
		},
	}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if ok {
		t.Fatalf("CredentialValid = true when batch credential is invalid")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

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

	mock.ExpectQuery(regexp.QuoteMeta("SELECT PackInstallId")).
		WithArgs("pack", "active").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}))

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if ok {
		t.Fatalf("CredentialValid = true without a converged active install")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}

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

	mock.ExpectQuery(regexp.QuoteMeta("SELECT PackInstallId")).
		WithArgs("pack", "active").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow("install-active"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Status, COALESCE(AppliedPayloadHash, ''), COALESCE(DesiredPayloadHash, '')")).
		WithArgs("pack", "install-active").
		WillReturnRows(sqlmock.NewRows([]string{"Status", "AppliedPayloadHash", "DesiredPayloadHash"}).AddRow("active", "hash", "hash"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(*)")).
		WithArgs("pack", "install-active").
		WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))

	ok, err := NewCredentialStatusStore(db, &fakeCredentialResolver{}).CredentialValid(context.Background(), "pack")
	if err != nil {
		t.Fatalf("CredentialValid: %v", err)
	}
	if ok {
		t.Fatalf("CredentialValid = true with live profiles from another install")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations: %v", err)
	}
}
