package handlers_test

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/clock"
	"github.com/yan1h/agent-coding-workflow/internal/jobs"
	"github.com/yan1h/agent-coding-workflow/internal/jobs/handlers"
)

func mkPayload(t *testing.T, topic string) json.RawMessage {
	t.Helper()
	p, err := json.Marshal(map[string]any{
		"topic": topic, "severity": "error", "title": "x", "body": "y",
		"metadata":    map[string]any{"k": "v"},
		"occurred_at": time.Now().UTC().Format(time.RFC3339),
	})
	require.NoError(t, err)
	return p
}

func TestWebhook_2xxCompletes(t *testing.T) {
	t.Parallel()
	got := make(chan struct{}, 1)
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got <- struct{}{}
		w.WriteHeader(http.StatusOK)
	}))
	defer ts.Close()

	h := handlers.NewWebhookHandler(handlers.WebhookConfig{
		URL: ts.URL, Secret: "s3cr3t", Timeout: time.Second,
	}, clock.Real())
	err := h.Handle(context.Background(), jobs.Job{
		ID: uuid.New(), Type: jobs.TypeWebhookDeliver, Payload: mkPayload(t, "x"),
	})
	require.NoError(t, err)
	<-got
}

func TestWebhook_4xxPermanent(t *testing.T) {
	t.Parallel()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte("bad token"))
	}))
	defer ts.Close()
	h := handlers.NewWebhookHandler(handlers.WebhookConfig{URL: ts.URL, Secret: "s", Timeout: time.Second}, clock.Real())
	err := h.Handle(context.Background(), jobs.Job{ID: uuid.New(), Type: jobs.TypeWebhookDeliver, Payload: mkPayload(t, "x")})
	require.Error(t, err)
	assert.True(t, jobs.IsPermanent(err), "4xx should be permanent")
	assert.Contains(t, err.Error(), "401")
}

func TestWebhook_5xxTransient(t *testing.T) {
	t.Parallel()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer ts.Close()
	h := handlers.NewWebhookHandler(handlers.WebhookConfig{URL: ts.URL, Secret: "s", Timeout: time.Second}, clock.Real())
	err := h.Handle(context.Background(), jobs.Job{ID: uuid.New(), Type: jobs.TypeWebhookDeliver, Payload: mkPayload(t, "x")})
	require.Error(t, err)
	assert.False(t, jobs.IsPermanent(err))
}

func TestWebhook_HMACSignatureValidByReceiver(t *testing.T) {
	t.Parallel()
	const secret = "s3cr3t"
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		ts := r.Header.Get("X-ACW-Timestamp")
		sig := r.Header.Get("X-ACW-Signature")

		mac := hmac.New(sha256.New, []byte(secret))
		mac.Write([]byte(ts + "." + string(body)))
		want := "sha256=" + hex.EncodeToString(mac.Sum(nil))
		if !hmac.Equal([]byte(want), []byte(sig)) {
			http.Error(w, "bad sig", http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer ts.Close()
	h := handlers.NewWebhookHandler(handlers.WebhookConfig{URL: ts.URL, Secret: secret, Timeout: time.Second}, clock.Real())
	err := h.Handle(context.Background(), jobs.Job{ID: uuid.New(), Type: jobs.TypeWebhookDeliver, Payload: mkPayload(t, "x")})
	require.NoError(t, err)
}

func TestWebhook_HeadersIncluded(t *testing.T) {
	t.Parallel()
	got := make(chan http.Header, 1)
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		got <- r.Header.Clone()
		w.WriteHeader(http.StatusOK)
	}))
	defer ts.Close()
	id := uuid.New()
	h := handlers.NewWebhookHandler(handlers.WebhookConfig{URL: ts.URL, Secret: "s", Timeout: time.Second}, clock.Real())
	err := h.Handle(context.Background(), jobs.Job{ID: id, Type: jobs.TypeWebhookDeliver, Payload: mkPayload(t, "workspace.sync_failed")})
	require.NoError(t, err)
	hdr := <-got
	assert.Equal(t, "workspace.sync_failed", hdr.Get("X-ACW-Topic"))
	assert.Equal(t, id.String(), hdr.Get("X-ACW-Event-Id"))
	assert.True(t, strings.HasPrefix(hdr.Get("X-ACW-Signature"), "sha256="))
	assert.NotEmpty(t, hdr.Get("X-ACW-Timestamp"))
}
