package catalogread

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"regexp"
	"testing"

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

const servingCapabilityQuery = `SELECT i.OrgId, i.AppInstallationId, i.ActiveReleaseDigest, i.CatalogRevision, i.State,
b.OrgId, b.AppInstallationId, b.AppReleaseDigest, b.CapabilityId, b.WorkloadId, b.ServiceReleaseDigest,
b.OwnershipEpoch, b.CompatibilityReceiptDigest
FROM _System_CatalogAppInstallation AS i
LEFT JOIN _System_CatalogHandlerBinding AS b
ON b.AppInstallationId = i.AppInstallationId AND b.CapabilityId = ? AND b.State = 'ACTIVE' AND b.ActiveKey IS NOT NULL
WHERE i.AppInstallationId = ?`

func TestNewRequiresDatabase(t *testing.T) {
	if _, err := New(nil); err == nil {
		t.Fatal("New(nil) succeeded")
	}
}

func TestReadServingCapabilityUsesOneStatementSnapshot(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	reader, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectQuery(regexp.QuoteMeta(servingCapabilityQuery)).WithArgs("capability-1", "installation-1").WillReturnRows(
		sqlmock.NewRows([]string{"i.OrgId", "i.AppInstallationId", "i.ActiveReleaseDigest", "i.CatalogRevision", "i.State", "b.OrgId", "b.AppInstallationId", "b.AppReleaseDigest", "b.CapabilityId", "b.WorkloadId", "b.ServiceReleaseDigest", "b.OwnershipEpoch", "b.CompatibilityReceiptDigest"}).
			AddRow("org-1", "installation-1", "sha256:release", []byte("7"), "ACTIVE", "org-1", "installation-1", "sha256:release", "capability-1", "runtime-api", "sha256:service", "9", "sha256:receipt"))

	capability, err := reader.ReadServingCapability(context.Background(), "installation-1", "capability-1")
	if err != nil {
		t.Fatal(err)
	}
	want := ServingCapability{
		Installation: ServingInstallation{OrgID: "org-1", AppInstallationID: "installation-1", ActiveReleaseDigest: "sha256:release", CatalogRevision: 7},
		Binding:      ServingHandlerBinding{OrgID: "org-1", AppInstallationID: "installation-1", AppReleaseDigest: "sha256:release", CapabilityID: "capability-1", WorkloadID: "runtime-api", ServiceReleaseDigest: "sha256:service", OwnershipEpoch: 9, CompatibilityReceiptDigest: "sha256:receipt"},
	}
	if capability != want {
		t.Fatalf("capability = %#v, want %#v", capability, want)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadServingCapabilityRejectsUnavailableInstallationAndBinding(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	reader, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectQuery(regexp.QuoteMeta(servingCapabilityQuery)).WithArgs("capability-1", "inactive").WillReturnRows(
		sqlmock.NewRows([]string{"i.OrgId", "i.AppInstallationId", "i.ActiveReleaseDigest", "i.CatalogRevision", "i.State", "b.OrgId", "b.AppInstallationId", "b.AppReleaseDigest", "b.CapabilityId", "b.WorkloadId", "b.ServiceReleaseDigest", "b.OwnershipEpoch", "b.CompatibilityReceiptDigest"}).
			AddRow("org-1", "inactive", nil, []byte("8"), "UPGRADING", nil, nil, nil, nil, nil, nil, nil, nil))
	if _, err := reader.ReadServingCapability(context.Background(), "inactive", "capability-1"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("inactive installation error = %v, want ErrNotFound", err)
	}
	mock.ExpectQuery(regexp.QuoteMeta(servingCapabilityQuery)).WithArgs("capability-1", "active-no-binding").WillReturnRows(
		sqlmock.NewRows([]string{"i.OrgId", "i.AppInstallationId", "i.ActiveReleaseDigest", "i.CatalogRevision", "i.State", "b.OrgId", "b.AppInstallationId", "b.AppReleaseDigest", "b.CapabilityId", "b.WorkloadId", "b.ServiceReleaseDigest", "b.OwnershipEpoch", "b.CompatibilityReceiptDigest"}).
			AddRow("org-1", "active-no-binding", "sha256:release", []byte("8"), "ACTIVE", nil, nil, nil, nil, nil, nil, nil, nil))
	if _, err := reader.ReadServingCapability(context.Background(), "active-no-binding", "capability-1"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("active installation without binding error = %v, want ErrNotFound", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestReadServingCapabilityFailsClosedForMissingOrMalformedFacts(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	reader, err := New(db)
	if err != nil {
		t.Fatal(err)
	}
	query := regexp.QuoteMeta(servingCapabilityQuery)
	mock.ExpectQuery(query).WithArgs("capability-1", "missing").WillReturnError(sql.ErrNoRows)
	if _, err := reader.ReadServingCapability(context.Background(), "missing", "capability-1"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("missing error = %v", err)
	}
	mock.ExpectQuery(query).WithArgs("capability-1", "malformed").WillReturnRows(
		sqlmock.NewRows([]string{"i.OrgId", "i.AppInstallationId", "i.ActiveReleaseDigest", "i.CatalogRevision", "i.State", "b.OrgId", "b.AppInstallationId", "b.AppReleaseDigest", "b.CapabilityId", "b.WorkloadId", "b.ServiceReleaseDigest", "b.OwnershipEpoch", "b.CompatibilityReceiptDigest"}).
			AddRow("org-1", "malformed", "sha256:release", []byte("7"), "ACTIVE", "org-1", "malformed", "sha256:release", "capability-1", nil, "sha256:service", "9", "sha256:receipt"))
	if _, err := reader.ReadServingCapability(context.Background(), "malformed", "capability-1"); err == nil {
		t.Fatal("incomplete active binding was accepted")
	}
	for _, test := range []struct {
		name string
		row  []driver.Value
	}{
		{
			name: "active installation without selected release",
			row:  []driver.Value{"org-1", "missing-release", nil, []byte("7"), "ACTIVE", nil, nil, nil, nil, nil, nil, nil, nil},
		},
		{
			name: "cross-org binding",
			row:  []driver.Value{"org-1", "cross-org", "sha256:release", []byte("7"), "ACTIVE", "org-2", "cross-org", "sha256:release", "capability-1", "runtime-api", "sha256:service", "9", "sha256:receipt"},
		},
		{
			name: "stale release binding",
			row:  []driver.Value{"org-1", "stale-release", "sha256:release", []byte("7"), "ACTIVE", "org-1", "stale-release", "sha256:stale", "capability-1", "runtime-api", "sha256:service", "9", "sha256:receipt"},
		},
		{
			name: "blank binding workload",
			row:  []driver.Value{"org-1", "blank-workload", "sha256:release", []byte("7"), "ACTIVE", "org-1", "blank-workload", "sha256:release", "capability-1", "", "sha256:service", "9", "sha256:receipt"},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			installationID := test.row[1].(string)
			mock.ExpectQuery(query).WithArgs("capability-1", installationID).WillReturnRows(
				sqlmock.NewRows([]string{"i.OrgId", "i.AppInstallationId", "i.ActiveReleaseDigest", "i.CatalogRevision", "i.State", "b.OrgId", "b.AppInstallationId", "b.AppReleaseDigest", "b.CapabilityId", "b.WorkloadId", "b.ServiceReleaseDigest", "b.OwnershipEpoch", "b.CompatibilityReceiptDigest"}).
					AddRow(test.row...))
			if _, err := reader.ReadServingCapability(context.Background(), installationID, "capability-1"); err == nil {
				t.Fatal("malformed serving capability was accepted")
			}
		})
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
