package service_test

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/service"
)

type recordedWebhookRequest struct {
	Header http.Header
	Body   string
}

func waitForWebhookCondition(t *testing.T, condition func() bool) {
	t.Helper()
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if condition() {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatal("timed out waiting for asynchronous webhook delivery")
}

func TestDispatchWebhookEventPersistsAndPostsMatchingHook(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	setupRepoForTest(t, svc, tenantHandle.Context(), "whdelivery", "repo")

	var repo db.Repository
	if err := tenantHandle.DB().Preload("Owner").First(&repo, "full_name = ?", "whdelivery/repo").Error; err != nil {
		t.Fatalf("find repo: %v", err)
	}

	requests := make(chan recordedWebhookRequest, 2)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		requests <- recordedWebhookRequest{
			Header: r.Header.Clone(),
			Body:   string(body),
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"ok":true}`))
	}))
	defer server.Close()

	pushHook := &db.Webhook{
		RepositoryID: repo.ID,
		Name:         "web",
		Active:       true,
		EventsJSON:   `["push"]`,
		ConfigJSON:   `{"url":"` + server.URL + `","content_type":"json","secret":"topsecret"}`,
	}
	issuesHook := &db.Webhook{
		RepositoryID: repo.ID,
		Name:         "web",
		Active:       true,
		EventsJSON:   `["issues"]`,
		ConfigJSON:   `{"url":"` + server.URL + `","content_type":"json"}`,
	}
	if err := svc.CreateWebhook(ctx, pushHook); err != nil {
		t.Fatalf("create push hook: %v", err)
	}
	if err := svc.CreateWebhook(ctx, issuesHook); err != nil {
		t.Fatalf("create issues hook: %v", err)
	}

	payload := map[string]any{"ref": "refs/heads/main", "after": "abc123"}
	if err := svc.DispatchWebhookEvent(ctx, repo.ID, "push", "", payload); err != nil {
		t.Fatalf("dispatch webhook event: %v", err)
	}

	var req recordedWebhookRequest
	select {
	case req = <-requests:
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for outbound webhook request")
	}

	if got := req.Header.Get("X-GitHub-Event"); got != "push" {
		t.Fatalf("X-GitHub-Event: got %q", got)
	}
	if got := req.Header.Get("Content-Type"); got != "application/json" {
		t.Fatalf("Content-Type: got %q", got)
	}
	if got := req.Header.Get("X-Hub-Signature-256"); got == "" {
		t.Fatal("expected X-Hub-Signature-256 header")
	}

	var received map[string]any
	if err := json.Unmarshal([]byte(req.Body), &received); err != nil {
		t.Fatalf("unmarshal request body: %v", err)
	}
	if received["ref"] != "refs/heads/main" {
		t.Fatalf("payload ref: got %v", received["ref"])
	}

	deliveries := waitForWebhookDeliveries(t, svc, ctx, repo.ID, pushHook.ID, 1)
	if deliveries[0].Status != "ok" {
		t.Fatalf("delivery status: got %q", deliveries[0].Status)
	}

	otherDeliveries, err := svc.ListHookDeliveries(ctx, repo.ID, issuesHook.ID)
	if err != nil {
		t.Fatalf("list issues hook deliveries: %v", err)
	}
	if len(otherDeliveries) != 0 {
		t.Fatalf("expected 0 deliveries for non-matching hook, got %d", len(otherDeliveries))
	}
}

func TestRedeliverHookDeliveryCreatesNewAttempt(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	setupRepoForTest(t, svc, tenantHandle.Context(), "whredeliver", "repo")

	var repo db.Repository
	if err := tenantHandle.DB().Preload("Owner").First(&repo, "full_name = ?", "whredeliver/repo").Error; err != nil {
		t.Fatalf("find repo: %v", err)
	}

	requests := make(chan struct{}, 2)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests <- struct{}{}
		w.WriteHeader(http.StatusAccepted)
		_, _ = w.Write([]byte("accepted"))
	}))
	defer server.Close()

	hook := &db.Webhook{
		RepositoryID: repo.ID,
		Name:         "web",
		Active:       true,
		EventsJSON:   `["push"]`,
		ConfigJSON:   `{"url":"` + server.URL + `","content_type":"json"}`,
	}
	if err := svc.CreateWebhook(ctx, hook); err != nil {
		t.Fatalf("create hook: %v", err)
	}
	if err := svc.DispatchWebhookEvent(ctx, repo.ID, "push", "", map[string]any{"after": "abc123"}); err != nil {
		t.Fatalf("dispatch webhook event: %v", err)
	}
	waitForWebhookRequest(t, requests)

	deliveries := waitForWebhookDeliveries(t, svc, ctx, repo.ID, hook.ID, 1)

	redelivery, err := svc.RedeliverHookDelivery(ctx, repo.ID, hook.ID, deliveries[0].ID)
	if err != nil {
		t.Fatalf("redeliver hook delivery: %v", err)
	}
	waitForWebhookRequest(t, requests)
	if !redelivery.Redelivery {
		t.Fatal("expected redelivery flag to be true")
	}
	if redelivery.GUID != deliveries[0].GUID {
		t.Fatalf("expected GUID to be reused, got %q want %q", redelivery.GUID, deliveries[0].GUID)
	}
	waitForWebhookDeliveries(t, svc, ctx, repo.ID, hook.ID, 2)
}

func TestDispatchWebhookEventHonorsInsecureSSL(t *testing.T) {
	svc, tenantHandle, cleanup := setupTestService(t)
	defer cleanup()
	ctx := tenantHandle.Context()

	setupRepoForTest(t, svc, tenantHandle.Context(), "whinsecure", "repo")

	var repo db.Repository
	if err := tenantHandle.DB().Preload("Owner").First(&repo, "full_name = ?", "whinsecure/repo").Error; err != nil {
		t.Fatalf("find repo: %v", err)
	}

	requests := make(chan struct{}, 2)
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests <- struct{}{}
		w.WriteHeader(http.StatusNoContent)
	}))
	defer server.Close()

	strictHook := &db.Webhook{
		RepositoryID: repo.ID,
		Name:         "web",
		Active:       true,
		EventsJSON:   `["push"]`,
		ConfigJSON:   `{"url":"` + server.URL + `","content_type":"json","insecure_ssl":"0"}`,
	}
	insecureHook := &db.Webhook{
		RepositoryID: repo.ID,
		Name:         "web",
		Active:       true,
		EventsJSON:   `["push"]`,
		ConfigJSON:   `{"url":"` + server.URL + `","content_type":"json","insecure_ssl":"1"}`,
	}
	if err := svc.CreateWebhook(ctx, strictHook); err != nil {
		t.Fatalf("create strict hook: %v", err)
	}
	if err := svc.CreateWebhook(ctx, insecureHook); err != nil {
		t.Fatalf("create insecure hook: %v", err)
	}

	if err := svc.DispatchWebhookEvent(ctx, repo.ID, "push", "", map[string]any{"after": "abc123"}); err != nil {
		t.Fatalf("dispatch webhook event: %v", err)
	}

	strictDeliveries := waitForWebhookDeliveries(t, svc, ctx, repo.ID, strictHook.ID, 1)
	if strictDeliveries[0].Status != "error" {
		t.Fatalf("expected strict delivery to fail TLS verification, got %q", strictDeliveries[0].Status)
	}

	insecureDeliveries := waitForWebhookDeliveries(t, svc, ctx, repo.ID, insecureHook.ID, 1)
	if insecureDeliveries[0].Status != "ok" {
		t.Fatalf("expected insecure delivery to succeed, got %q", insecureDeliveries[0].Status)
	}
	if got := len(requests); got != 1 {
		t.Fatalf("expected only insecure delivery to reach the TLS server, got %d hits", got)
	}
}

func waitForWebhookRequest(t *testing.T, requests <-chan struct{}) {
	t.Helper()
	select {
	case <-requests:
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for outbound webhook request")
	}
}

func waitForWebhookDeliveries(
	t *testing.T,
	svc *service.Service,
	ctx context.Context,
	repoID, hookID uint,
	want int,
) []db.HookDelivery {
	t.Helper()
	deadline := time.Now().Add(5 * time.Second)
	for {
		deliveries, err := svc.ListHookDeliveries(ctx, repoID, hookID)
		if err == nil && len(deliveries) == want {
			complete := true
			for _, delivery := range deliveries {
				if delivery.Status == "pending" {
					complete = false
					break
				}
			}
			if complete {
				return deliveries
			}
		}
		if time.Now().After(deadline) {
			t.Fatalf("timed out waiting for %d completed webhook deliveries: got %d, err=%v", want, len(deliveries), err)
		}
		time.Sleep(10 * time.Millisecond)
	}
}
