package connector_webhooks

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	connectorcapabilities "github.com/pingcap-inc/nexuscrm-connectors/features/connector/capabilities"
	connector_events "github.com/pingcap-inc/nexuscrm-connectors/features/connector/events"
)

// TestDispatcher_VerifierInputPopulatesMethodRequestURIAndRequestURL asserts that the
// dispatcher populates VerifierInput.Method from the inbound method and
// VerifierInput.RequestURI / RequestURL from server-owned request values before
// calling Verify.
func TestDispatcher_VerifierInputPopulatesMethodRequestURIAndRequestURL(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "")

	verifier := &fakeVerifier{} // records the VerifierInput it receives
	parser := &fakeParser{
		events: []connectorcapabilities.ConnectorEvent{
			{EventType: "contact.created", Namespace: testNamespace, ObjectKind: "contact", SourceID: "src-meth-uri-1", Payload: []byte(`{}`), IdempotencyKey: "meth-uri-evt"},
		},
	}
	d := &Dispatcher{
		resolvePack:   stubPackActive(),
		packTransient: func(context.Context, string) (bool, error) { return false, nil },
		resolveSecret: stubSecret("hmac-sha256", []byte("s")),
		registry:      buildRegistry(testNamespace, true, verifier, parser),
		enqueueBatch: func(_ context.Context, _, _, _ string, events []connector_events.BatchEvent) ([]string, []bool, error) {
			_ids := make([]string, len(events))
			for _bi := range events {
				_ids[_bi] = "evt-id"
			}
			return _ids, make([]bool, len(events)), nil
		},
		routes:       noopRoutes,
		maxBodyBytes: maxInboundWebhookBodyBytes,
	}

	w := postRequest(d, testNamespace, testSecretName, `{}`)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202", w.Code)
	}
	if verifier.got == nil {
		t.Fatal("verifier.got is nil — Verify was not called")
	}
	if verifier.got.Method == "" {
		t.Fatal("verifier.got.Method is empty; dispatcher did not populate VerifierInput.Method from c.Request.Method")
	}
	if verifier.got.RequestURI == "" {
		t.Fatal("verifier.got.RequestURI is empty; dispatcher did not populate VerifierInput.RequestURI with the raw request target")
	}
	if verifier.got.RequestURL == "" {
		t.Fatal("verifier.got.RequestURL is empty; dispatcher did not populate VerifierInput.RequestURL with the full signing URL")
	}
	if verifier.got.Method != http.MethodPost {
		t.Fatalf("verifier.got.Method = %q, want %q", verifier.got.Method, http.MethodPost)
	}
	if verifier.got.RequestURI != "/webhooks/"+testNamespace+"/"+testSecretName {
		t.Fatalf("verifier.got.RequestURI = %q, want raw request target", verifier.got.RequestURI)
	}
	if verifier.got.RequestURL != "http://example.com/webhooks/"+testNamespace+"/"+testSecretName {
		t.Fatalf("verifier.got.RequestURL = %q, want direct full URL", verifier.got.RequestURL)
	}
}

func TestDispatcher_RequestURITokenPreservesRawRequestTargetWithConfiguredBase(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "https://public.example.test/nexuscrm")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, requestTarget, time.Now())

	d := dispatcherWithVerifier(newTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_ConfiguredWebhookBaseURLBuildsFullSigningURL(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "https://public.example.test/nexuscrm")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	canonicalURL := "https://public.example.test/nexuscrm" + requestTarget
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, canonicalURL, time.Now())
	headers.Set("X-Forwarded-Proto", "http")
	headers.Set("X-Forwarded-Host", "attacker.example.test")

	d := dispatcherWithVerifier(newFullURLTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_RequestURITokenDoesNotRequireValidRequestURLBase(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "://bad")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, requestTarget, time.Now())

	d := dispatcherWithVerifier(newTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_ConfiguredWebhookBaseURLIgnoresSpoofedForwardedHost(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "https://public.example.test")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	attackerURL := "http://attacker.example.test" + requestTarget
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, attackerURL, time.Now())
	headers.Set("X-Forwarded-Proto", "http")
	headers.Set("X-Forwarded-Host", "attacker.example.test")

	d := dispatcherWithVerifier(newFullURLTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_RequestURLTokenFailsClosedWhenBaseIsInvalid(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "://bad")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, "https://public.example.test"+requestTarget, time.Now())

	d := dispatcherWithVerifier(newFullURLTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_APIBaseURLFallbackBuildsFullSigningURL(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "https://api.example.test/base")

	verifier := &fakeVerifier{}
	parser := &fakeParser{
		events: []connectorcapabilities.ConnectorEvent{
			{EventType: "contact.created", Namespace: testNamespace, ObjectKind: "contact", SourceID: "src-api-base", Payload: []byte(`{}`), IdempotencyKey: "api-base-evt"},
		},
	}
	d := &Dispatcher{
		resolvePack:   stubPackActive(),
		packTransient: func(context.Context, string) (bool, error) { return false, nil },
		resolveSecret: stubSecret("hmac-sha256", []byte("s")),
		registry:      buildRegistry(testNamespace, true, verifier, parser),
		enqueueBatch: func(_ context.Context, _, _, _ string, events []connector_events.BatchEvent) ([]string, []bool, error) {
			_ids := make([]string, len(events))
			for _bi := range events {
				_ids[_bi] = "evt-id"
			}
			return _ids, make([]bool, len(events)), nil
		},
		routes:       noopRoutes,
		maxBodyBytes: maxInboundWebhookBodyBytes,
	}

	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	w := postRequestTarget(d, requestTarget, `{}`, nil)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
	if verifier.got == nil {
		t.Fatal("verifier.got is nil")
	}
	if verifier.got.RequestURI != requestTarget {
		t.Fatalf("verifier.got.RequestURI = %q, want raw request target", verifier.got.RequestURI)
	}
	if verifier.got.RequestURL != "https://api.example.test/base"+requestTarget {
		t.Fatalf("verifier.got.RequestURL = %q, want API_BASE_URL full URL", verifier.got.RequestURL)
	}
}

func TestDispatcher_DirectTLSHostFallbackBuildsFullSigningURL(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	directURL := "https://direct.example.test" + requestTarget
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, directURL, time.Now())

	d := dispatcherWithVerifier(newFullURLTemplateVerifier(t), secret)
	w := postRequestTarget(d, directURL, string(body), headers)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
}

func TestDispatcher_RequestURLTokenRejectsForwardedHeaderWithoutConfiguredBase(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	directURL := "https://direct.example.test" + requestTarget
	secret := []byte("signed-secret")

	for _, tc := range []struct {
		name  string
		key   string
		value string
	}{
		{name: "forwarded", key: "Forwarded", value: "host=proxy.example.test;proto=https"},
		{name: "x-forwarded-host", key: "X-Forwarded-Host", value: "proxy.example.test"},
		{name: "x-forwarded-proto", key: "X-Forwarded-Proto", value: "https"},
		{name: "x-forwarded-for", key: "X-Forwarded-For", value: "203.0.113.10"},
		{name: "blank-forwarded", key: "Forwarded", value: ""},
		{name: "blank-x-forwarded-host", key: "X-Forwarded-Host", value: "   "},
	} {
		t.Run(tc.name, func(t *testing.T) {
			headers := signedWebhookHeaders(body, secret, http.MethodPost, directURL, time.Now())
			headers.Set(tc.key, tc.value)

			d := dispatcherWithVerifier(newFullURLTemplateVerifier(t), secret)
			w := postRequestTarget(d, requestTarget, string(body), headers)

			if w.Code != http.StatusServiceUnavailable {
				t.Fatalf("status = %d, want 503; body=%s", w.Code, w.Body.String())
			}
		})
	}
}

func TestDispatcher_RequestURITokenAllowsForwardedHeaderWithoutConfiguredBase(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "")

	body := []byte(`{"event":"contact.created"}`)
	requestTarget := "/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"
	secret := []byte("signed-secret")

	headers := signedWebhookHeaders(body, secret, http.MethodPost, requestTarget, time.Now())
	headers.Set("X-Forwarded-Host", "proxy.example.test")

	d := dispatcherWithVerifier(newTemplateVerifier(t), secret)
	w := postRequestTarget(d, requestTarget, string(body), headers)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status = %d, want 202; body=%s", w.Code, w.Body.String())
	}
}

func TestResolveWebhookRequestURLRejectsMissingDirectHost(t *testing.T) {
	t.Setenv("WEBHOOK_BASE_URL", "")
	t.Setenv("API_BASE_URL", "")

	req := httptest.NewRequest(http.MethodPost, "/webhooks/"+testNamespace+"/"+testSecretName, nil)
	req.Host = ""

	if got, err := resolveWebhookRequestURL(req); err == nil {
		t.Fatalf("resolveWebhookRequestURL = %q, nil error; want missing-host error", got)
	}
}

func TestResolveWebhookRequestURIRejectsAbsoluteFormRequestTarget(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "https://direct.example.test/webhooks/"+testNamespace+"/"+testSecretName+"?delivery=a%2Fb", nil)
	req.RequestURI = "https://direct.example.test/webhooks/" + testNamespace + "/" + testSecretName + "?delivery=a%2Fb"

	if got, err := resolveWebhookRequestURI(req); err == nil {
		t.Fatalf("resolveWebhookRequestURI = %q, nil error; want absolute-form rejection", got)
	}
}
