package connector_webhooks

import (
	"context"
	"fmt"
	"testing"

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

// ── gate 4: consent absent → error ─────────────────────────────────────────

// TestCheckCompanionConsent_NoRow_ConsentMissing (gate 4 §7):
// When no _System_InstalledPackDependency row exists, CheckCompanionConsent
// returns an error whose message contains "consent missing".
func TestCheckCompanionConsent_NoRow_ConsentMissing(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT COALESCE\(DependentPackInstallId`).
		WithArgs("companion-app", "provider-connector").
		WillReturnRows(sqlmock.NewRows([]string{"DependentPackInstallId", "VersionConstraint"}))

	consentErr := CheckCompanionConsent(context.Background(), db, "companion-app", "companion-install-v1", "provider-connector", "1.0.0")
	if consentErr == nil {
		t.Fatal("expected error for missing consent row, got nil")
	}
	if got := consentErr.Error(); len(got) == 0 {
		t.Fatal("consent error message is empty")
	}
}

// ── gate 5: dependency row exists but DependentPackInstallId drifted ──────

// TestCheckCompanionConsent_IncarnationDrift_ConsentMissing (gate 5 §7):
// When the dependency row's DependentPackInstallId doesn't match the current
// active companion PackInstallId, CheckCompanionConsent returns an error.
func TestCheckCompanionConsent_IncarnationDrift_ConsentMissing(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	// Row exists but was written for a stale PackInstallId.
	mock.ExpectQuery(`SELECT COALESCE\(DependentPackInstallId`).
		WithArgs("companion-app", "provider-connector").
		WillReturnRows(sqlmock.NewRows([]string{"DependentPackInstallId", "VersionConstraint"}).
			AddRow("stale-companion-install", nil))

	consentErr := CheckCompanionConsent(context.Background(), db, "companion-app", "current-companion-install", "provider-connector", "1.0.0")
	if consentErr == nil {
		t.Fatal("expected error for incarnation drift, got nil")
	}
}

// ── gate 6: version constraint unsatisfied ─────────────────────────────────

// TestCheckCompanionConsent_VersionConstraintUnsatisfied (gate 6 §7):
// When the VersionConstraint is not satisfied by the provider PackVersion,
// CheckCompanionConsent returns an error.
func TestCheckCompanionConsent_VersionConstraintUnsatisfied(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT COALESCE\(DependentPackInstallId`).
		WithArgs("companion-app", "provider-connector").
		WillReturnRows(sqlmock.NewRows([]string{"DependentPackInstallId", "VersionConstraint"}).
			AddRow("companion-install-v1", ">=2.0.0"))

	consentErr := CheckCompanionConsent(context.Background(), db, "companion-app", "companion-install-v1", "provider-connector", "1.0.0")
	if consentErr == nil {
		t.Fatalf("expected version constraint error for provider=1.0.0 constraint=>=2.0.0, got nil")
	}
}

// TestCheckCompanionConsent_VersionConstraintSatisfied (gate 6 inverse §7):
// When the constraint is satisfied, CheckCompanionConsent returns nil.
func TestCheckCompanionConsent_VersionConstraintSatisfied(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT COALESCE\(DependentPackInstallId`).
		WithArgs("companion-app", "provider-connector").
		WillReturnRows(sqlmock.NewRows([]string{"DependentPackInstallId", "VersionConstraint"}).
			AddRow("companion-install-v1", ">=1.0.0"))

	consentErr := CheckCompanionConsent(context.Background(), db, "companion-app", "companion-install-v1", "provider-connector", "1.5.0")
	if consentErr != nil {
		t.Fatalf("expected no error for satisfied constraint, got: %v", consentErr)
	}
}

// TestCheckCompanionConsent_NullConstraint_AnyVersionAccepted:
// When VersionConstraint is NULL, any provider version is accepted.
func TestCheckCompanionConsent_NullConstraint_AnyVersionAccepted(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT COALESCE\(DependentPackInstallId`).
		WithArgs("companion-app", "provider-connector").
		WillReturnRows(sqlmock.NewRows([]string{"DependentPackInstallId", "VersionConstraint"}).
			AddRow("companion-install-v1", nil))

	consentErr := CheckCompanionConsent(context.Background(), db, "companion-app", "companion-install-v1", "provider-connector", "0.1.0-alpha")
	if consentErr != nil {
		t.Fatalf("expected no error for null constraint (any version), got: %v", consentErr)
	}
}

// ── CI BLOCK regression: cross-pack predicate must not capture ownerless /
// same-pack / customized flows (those stay on the existing same-pack/legacy
// path with no companion snapshot). Only a non-customized flow owned by a
// DIFFERENT app pack is a cross-pack companion flow. ─────────────────────────

func TestResolveCompanionFlowIncarnation_NotCrossPack(t *testing.T) {
	cases := []struct {
		name        string
		ownerPack   string
		activeVer   string
		isCustom    bool
		nsNamespace string
	}{
		// Ownerless / admin-authored / legacy flow: OwnerPack='' must NOT be
		// treated as cross-pack (the CI HIGH regression — it produced a 503).
		{"ownerless", "", "flowver_x_v1", false, "provider-connector"},
		// Same-pack: companion IS the connector.
		{"same_pack", "provider-connector", "flowver_x_v1", false, "provider-connector"},
		{"same_pack_case_insensitive", "Provider-Connector", "flowver_x_v1", false, "provider-connector"},
		// Customized flow owned by another pack: not a protected pack-owned
		// standard flow, so it stays on the existing path (no consent enforcement).
		{"customized_other_pack", "companion-app", "flowver_x_v1", true, "provider-connector"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			defer db.Close()

			// ONLY the FlowDefinition lookup may run — no _System_InstalledPack
			// query, because these are not cross-pack.
			mock.ExpectQuery(`SELECT COALESCE\(OwnerPack`).
				WithArgs("flow-x").
				WillReturnRows(sqlmock.NewRows([]string{"OwnerPack", "ActiveVersionId", "IsCustomized"}).
					AddRow(tc.ownerPack, tc.activeVer, tc.isCustom))

			incr, isCross, rerr := ResolveCompanionFlowIncarnation(context.Background(), db, tc.nsNamespace, "flow-x")
			if rerr != nil {
				t.Fatalf("%s: expected nil error (same-pack/legacy path), got %v", tc.name, rerr)
			}
			if isCross {
				t.Fatalf("%s: expected NOT cross-pack, got cross-pack incr=%+v", tc.name, incr)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Errorf("%s: unmet expectations (a second query ran for a non-cross-pack flow?): %v", tc.name, err)
			}
		})
	}
}

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

	// Non-customized flow owned by a DIFFERENT active app pack → cross-pack.
	mock.ExpectQuery(`SELECT COALESCE\(OwnerPack`).
		WithArgs("flow-x").
		WillReturnRows(sqlmock.NewRows([]string{"OwnerPack", "ActiveVersionId", "IsCustomized"}).
			AddRow("companion-app", "flowver_x_v3", false))
	mock.ExpectQuery(`SELECT COALESCE\(PackInstallId`).
		WithArgs("companion-app", sqlmock.AnyArg(), "active").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow("companion-install-77"))

	incr, isCross, rerr := ResolveCompanionFlowIncarnation(context.Background(), db, "provider-connector", "flow-x")
	if rerr != nil {
		t.Fatalf("expected nil error, got %v", rerr)
	}
	if !isCross {
		t.Fatal("expected cross-pack, got not-cross-pack")
	}
	if incr.CompanionPackName != "companion-app" || incr.CompanionPackInstallID != "companion-install-77" || incr.CompanionFlowVersionID != "flowver_x_v3" {
		t.Errorf("unexpected incarnation: %+v", incr)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet expectations: %v", err)
	}
}

// ── AuthorizeCrossPackProfileDependency unit tests ──────────────────────────

// TestAuthorizeCrossPackProfileDependency_SamePack_NilDB_ReturnsNil confirms
// that same-pack (case-insensitive) passes immediately with nil db.
func TestAuthorizeCrossPackProfileDependency_SamePack_NilDB_ReturnsNil(t *testing.T) {
	if err := AuthorizeCrossPackProfileDependency(context.Background(), nil, "pack-a", "Pack-A"); err != nil {
		t.Errorf("same-pack with nil db must return nil, got: %v", err)
	}
}

// TestAuthorizeCrossPackProfileDependency_EmptyRequester_ReturnsError confirms
// that a blank requesterOwnerPack is rejected.
func TestAuthorizeCrossPackProfileDependency_EmptyRequester_ReturnsError(t *testing.T) {
	if err := AuthorizeCrossPackProfileDependency(context.Background(), nil, "", "pack-b"); err == nil {
		t.Fatal("expected error for empty requester, got nil")
	}
}

// TestAuthorizeCrossPackProfileDependency_EmptyProfile_ReturnsError confirms
// that a blank profileOwnerPack is rejected.
func TestAuthorizeCrossPackProfileDependency_EmptyProfile_ReturnsError(t *testing.T) {
	if err := AuthorizeCrossPackProfileDependency(context.Background(), nil, "pack-a", ""); err == nil {
		t.Fatal("expected error for empty profile, got nil")
	}
}

// TestAuthorizeCrossPackProfileDependency_CrossPack_NilDB_ReturnsError confirms
// that cross-pack with nil db returns an error containing "db is required".
func TestAuthorizeCrossPackProfileDependency_CrossPack_NilDB_ReturnsError(t *testing.T) {
	err := AuthorizeCrossPackProfileDependency(context.Background(), nil, "pack-a", "pack-b")
	if err == nil {
		t.Fatal("expected error for cross-pack with nil db, got nil")
	}
	if msg := err.Error(); len(msg) == 0 {
		t.Fatal("error message is empty")
	}
}

// Ensure package is importable.
var _ = fmt.Sprintf
