package packs

import (
	"context"
	"database/sql/driver"
	"encoding/json"
	"strings"
	"testing"

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

func validHandlerConfigSignatureJSON() string {
	return `{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "skewWindow":"5m",
  "secretName":"default"
}`
}

func mustNewHandlerConfigComp(t *testing.T, packName, kind, config string) PackComponent {
	t.Helper()
	payload := []byte(`{"namespace":"` + packName + `","capabilityKind":"` + kind + `","config":` + config + `}`)
	comp, err := newConnectorInboundHandlerConfigComponent(packName, ManifestComponent{
		Kind:          ComponentKindConnectorInboundHandlerConfig,
		BusinessKey:   packName + ":" + kind,
		Manageability: ManageabilityLocked,
	}, payload)
	if err != nil {
		t.Fatalf("newConnectorInboundHandlerConfigComponent: %v", err)
	}
	return comp
}

func TestConnectorInboundHandlerConfigComponent_BusinessKey(t *testing.T) {
	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", validHandlerConfigSignatureJSON())
	if got, want := comp.BusinessKey(), "acme-connector:signature_verifier"; got != want {
		t.Fatalf("BusinessKey() = %q, want %q", got, want)
	}
}

func TestConnectorInboundHandlerConfigComponent_ExpectedPayloadRoundtrip(t *testing.T) {
	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", validHandlerConfigSignatureJSON())
	raw, err := comp.ExpectedPayload()
	if err != nil {
		t.Fatalf("ExpectedPayload: %v", err)
	}
	var payload ConnectorInboundHandlerConfigComponentPayload
	if err := json.Unmarshal(raw, &payload); err != nil {
		t.Fatalf("unmarshal ExpectedPayload: %v", err)
	}
	if payload.Namespace != "acme-connector" || payload.CapabilityKind != "signature_verifier" || len(payload.Config) == 0 {
		t.Fatalf("payload = %+v, want namespace/kind/config", payload)
	}
}

func TestConnectorInboundHandlerConfigComponent_EndpointConfigsUsesSemanticCanonicalJSON(t *testing.T) {
	config := `{
  "endpointConfigs":{
    "  Endpoint-A  ":{
      "scheme":"hmac-sha256-template",
      "signingTemplate":"{Timestamp}.{RawBody}",
      "timestampHeader":"X-A-Timestamp",
      "signatureHeader":"X-A-Signature",
      "signatureEncoding":"hex",
      "skewWindow":"5m",
      "secretName":" endpoint-a "
    }
  }
}`
	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", config)
	projection, err := comp.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	row := projection.(connectorInboundHandlerConfigRow)
	if !strings.Contains(row.ConfigJSON, `"endpoint-a"`) {
		t.Fatalf("ExpectedProjection ConfigJSON = %q, want canonical lowercase endpoint key", row.ConfigJSON)
	}
	if strings.Contains(row.ConfigJSON, "Endpoint-A") || strings.Contains(row.ConfigJSON, " endpoint-a ") {
		t.Fatalf("ExpectedProjection ConfigJSON = %q, contains non-canonical endpoint spelling", row.ConfigJSON)
	}
}

func TestConnectorInboundHandlerConfigComponent_AcceptsRequestURLSigningToken(t *testing.T) {
	config := `{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Method}\n{RequestURL}\n{RawBody}\n{Timestamp}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "skewWindow":"5m",
  "secretName":"default"
}`
	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", config)
	projection, err := comp.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	if got := projection.(connectorInboundHandlerConfigRow).ConfigJSON; !strings.Contains(got, `{RequestURL}`) {
		t.Fatalf("ExpectedProjection ConfigJSON = %q, want RequestURL token retained", got)
	}
}

func TestConnectorInboundHandlerConfigComponent_RejectsUnknownConfigField(t *testing.T) {
	payload := []byte(`{"namespace":"acme-connector","capabilityKind":"signature_verifier","config":{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "skewWindow":"5m",
  "secretName":"default",
  "surprise":true
}}`)
	_, err := newConnectorInboundHandlerConfigComponent("acme-connector", ManifestComponent{
		Kind:          ComponentKindConnectorInboundHandlerConfig,
		BusinessKey:   "acme-connector:signature_verifier",
		Manageability: ManageabilityLocked,
	}, payload)
	if err == nil {
		t.Fatal("newConnectorInboundHandlerConfigComponent = nil, want unknown-field error")
	}
	if got := err.Error(); !strings.Contains(got, "surprise") {
		t.Fatalf("error %q does not mention unknown field", got)
	}
}

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

	mock.ExpectQuery("SELECT PackInstallId").
		WithArgs("acme-connector").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow("pack-install-1"))
	mock.ExpectExec("INSERT INTO _System_ConnectorInboundHandlerConfig").
		WithArgs(sqlmock.AnyArg(), "pack-install-1", "acme-connector", "signature_verifier", sqlmock.AnyArg()).
		WillReturnResult(sqlmock.NewResult(1, 1))

	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", validHandlerConfigSignatureJSON())
	if err := comp.Apply(context.Background(), ApplyDeps{DB: db}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	config := `{
  "endpointConfigs":{
    "  Endpoint-A  ":{
      "scheme":"hmac-sha256-template",
      "signingTemplate":"{Timestamp}.{RawBody}",
      "timestampHeader":"X-A-Timestamp",
      "signatureHeader":"X-A-Signature",
      "signatureEncoding":"hex",
      "skewWindow":"5m",
      "secretName":" endpoint-a "
    }
  }
}`
	mock.ExpectQuery("SELECT PackInstallId").
		WithArgs("acme-connector").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow("pack-install-1"))
	mock.ExpectExec("INSERT INTO _System_ConnectorInboundHandlerConfig").
		WithArgs(sqlmock.AnyArg(), "pack-install-1", "acme-connector", "signature_verifier", canonicalJSONArg{
			mustContain: []string{`"endpoint-a"`},
			mustReject:  []string{`Endpoint-A`, ` endpoint-a `},
		}).
		WillReturnResult(sqlmock.NewResult(1, 1))

	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", config)
	if err := comp.Apply(context.Background(), ApplyDeps{DB: db}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func handlerConfigComponentCanonicalJSON(t *testing.T, comp PackComponent) string {
	t.Helper()
	projection, err := comp.ExpectedProjection()
	if err != nil {
		t.Fatalf("ExpectedProjection: %v", err)
	}
	return projection.(connectorInboundHandlerConfigRow).ConfigJSON
}

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

	comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", validHandlerConfigSignatureJSON())
	packInstallID := "pack-install-1"
	mock.ExpectQuery("SELECT PackInstallId").
		WithArgs("acme-connector").
		WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow(packInstallID))
	mock.ExpectQuery("SELECT Id, InstalledPackId, Namespace, CapabilityKind, ConfigJSON").
		WithArgs(packInstallID, "signature_verifier").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "InstalledPackId", "Namespace", "CapabilityKind", "ConfigJSON", "IsDeleted"}).
			AddRow(connectorInboundHandlerConfigID(packInstallID, "signature_verifier"), packInstallID, "acme-connector", "signature_verifier", handlerConfigComponentCanonicalJSON(t, comp), 0))

	verifier := comp.(componentCustomVerifier)
	if err := verifier.VerifyComponent(context.Background(), VerifyDeps{DB: db}); err != nil {
		t.Fatalf("VerifyComponent: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func TestConnectorInboundHandlerConfigComponent_VerifyComponentDetectsDrift(t *testing.T) {
	for _, tc := range []struct {
		name      string
		rowID     string
		installID string
		namespace string
		kind      string
		config    string
		isDeleted int
		want      string
	}{
		{name: "missing row", want: "not found"},
		{name: "wrong id", rowID: "cihc_wrong", installID: "pack-install-1", namespace: "acme-connector", kind: "signature_verifier", config: "canonical", want: "Id mismatch"},
		{name: "wrong install id", rowID: "expected", installID: "pack-install-other", namespace: "acme-connector", kind: "signature_verifier", config: "canonical", want: "InstalledPackId mismatch"},
		{name: "wrong namespace", rowID: "expected", installID: "pack-install-1", namespace: "other-connector", kind: "signature_verifier", config: "canonical", want: "Namespace mismatch"},
		{name: "wrong kind", rowID: "expected", installID: "pack-install-1", namespace: "acme-connector", kind: "envelope_parser", config: "canonical", want: "CapabilityKind mismatch"},
		{name: "config drift", rowID: "expected", installID: "pack-install-1", namespace: "acme-connector", kind: "signature_verifier", config: `{"drift":true}`, want: "ConfigJSON drift"},
		{name: "soft deleted", rowID: "expected", installID: "pack-install-1", namespace: "acme-connector", kind: "signature_verifier", config: "canonical", isDeleted: 1, want: "soft-deleted"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			defer db.Close()

			comp := mustNewHandlerConfigComp(t, "acme-connector", "signature_verifier", validHandlerConfigSignatureJSON())
			packInstallID := "pack-install-1"
			canonical := handlerConfigComponentCanonicalJSON(t, comp)
			expectedID := connectorInboundHandlerConfigID(packInstallID, "signature_verifier")
			rowID := tc.rowID
			if rowID == "expected" {
				rowID = expectedID
			}
			config := tc.config
			if config == "canonical" {
				config = canonical
			}

			mock.ExpectQuery("SELECT PackInstallId").
				WithArgs("acme-connector").
				WillReturnRows(sqlmock.NewRows([]string{"PackInstallId"}).AddRow(packInstallID))
			rowQuery := mock.ExpectQuery("SELECT Id, InstalledPackId, Namespace, CapabilityKind, ConfigJSON").
				WithArgs(packInstallID, "signature_verifier")
			if tc.name == "missing row" {
				rowQuery.WillReturnRows(sqlmock.NewRows([]string{"Id", "InstalledPackId", "Namespace", "CapabilityKind", "ConfigJSON", "IsDeleted"}))
			} else {
				rowQuery.WillReturnRows(sqlmock.NewRows([]string{"Id", "InstalledPackId", "Namespace", "CapabilityKind", "ConfigJSON", "IsDeleted"}).
					AddRow(rowID, tc.installID, tc.namespace, tc.kind, config, tc.isDeleted))
			}

			verifier := comp.(componentCustomVerifier)
			err = verifier.VerifyComponent(context.Background(), VerifyDeps{DB: db})
			if err == nil {
				t.Fatal("VerifyComponent = nil, want drift error")
			}
			if !strings.Contains(err.Error(), tc.want) {
				t.Fatalf("VerifyComponent error %q does not contain %q", err, tc.want)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("sql expectations: %v", err)
			}
		})
	}
}

type canonicalJSONArg struct {
	mustContain []string
	mustReject  []string
}

func (a canonicalJSONArg) Match(v driver.Value) bool {
	s, ok := v.(string)
	if !ok {
		return false
	}
	for _, want := range a.mustContain {
		if !strings.Contains(s, want) {
			return false
		}
	}
	for _, reject := range a.mustReject {
		if strings.Contains(s, reject) {
			return false
		}
	}
	return true
}
