package e2e

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

	"github.com/bridge/ai-customer-service/internal/app"
	"github.com/bridge/ai-customer-service/internal/config"
	"github.com/bridge/ai-customer-service/internal/http/handlers"
	"github.com/bridge/ai-customer-service/internal/platform/logging"
)

func newTestAppWithSecret(t *testing.T) *app.App {
	t.Helper()
	cfg := &config.Config{}
	cfg.HTTP.Addr = ":0"
	cfg.HTTP.ReadHeaderTimeout = 5
	cfg.HTTP.ReadTimeout = 10
	cfg.HTTP.WriteTimeout = 15
	cfg.HTTP.IdleTimeout = 60
	cfg.HTTP.MaxHeaderBytes = 1 << 20
	cfg.HTTP.MaxBodyBytes = 1 << 20
	cfg.Webhook.Secret = "e2e-test-secret"
	cfg.Webhook.TimestampHeader = "X-CS-Timestamp"
	cfg.Webhook.SignatureHeader = "X-CS-Signature"
	cfg.Webhook.MaxSkewSeconds = 300
	cfg.Runtime.Env = "test"
	application, err := app.New(cfg, logging.New())
	if err != nil {
		t.Fatalf("app.New() error = %v", err)
	}
	return application
}

// TestSecurity_InvalidSignature verifies that a request with a wrong signature
// is rejected with 403 and error code CS_AUTH_4034.
func TestSecurity_InvalidSignature(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	body := []byte(`{"message_id":"m-sec-1","channel":"widget","open_id":"u_sec","content":"查询额度"}`)
	timestamp, _, err := handlers.SignWebhookRequest("e2e-test-secret", time.Now().Unix(), body)
	if err != nil {
		t.Fatalf("SignWebhookRequest error = %v", err)
	}

	// Use a deliberately wrong signature value
	wrongSig := "deadbeefcafebabe0000000000000000000000000000000000000000000000"
	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	req.Header.Set("X-CS-Signature", wrongSig)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", resp.StatusCode)
	}

	bodyOut, _ := io.ReadAll(resp.Body)
	var errPayload map[string]any
	if err := json.Unmarshal(bodyOut, &errPayload); err != nil {
		t.Fatalf("decode error response error = %v", err)
	}
	errObj := errPayload["error"].(map[string]any)
	code := errObj["code"].(string)
	if code != "CS_AUTH_4034" {
		t.Fatalf("error code = %s, want CS_AUTH_4034", code)
	}
}

// TestSecurity_MissingSignature verifies that a request without the signature
// header is rejected with 403 and error code CS_AUTH_4031.
func TestSecurity_MissingSignature(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	body := []byte(`{"message_id":"m-sec-2","channel":"widget","open_id":"u_sec","content":"查询额度"}`)
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)

	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	// Intentionally omit X-CS-Signature

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", resp.StatusCode)
	}

	bodyOut, _ := io.ReadAll(resp.Body)
	var errPayload map[string]any
	if err := json.Unmarshal(bodyOut, &errPayload); err != nil {
		t.Fatalf("decode error response error = %v", err)
	}
	errObj := errPayload["error"].(map[string]any)
	code := errObj["code"].(string)
	if code != "CS_AUTH_4031" {
		t.Fatalf("error code = %s, want CS_AUTH_4031", code)
	}
}

// TestSecurity_ExpiredTimestamp verifies that a request with a stale timestamp
// is rejected with 403 and error code CS_AUTH_4033.
func TestSecurity_ExpiredTimestamp(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	body := []byte(`{"message_id":"m-sec-3","channel":"widget","open_id":"u_sec","content":"查询额度"}`)
	// Timestamp 10 minutes in the past — beyond the 5-minute MaxSkew
	staleUnix := time.Now().Add(-10 * time.Minute).Unix()
	timestamp, signature, err := handlers.SignWebhookRequest("e2e-test-secret", staleUnix, body)
	if err != nil {
		t.Fatalf("SignWebhookRequest error = %v", err)
	}

	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	req.Header.Set("X-CS-Signature", signature)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", resp.StatusCode)
	}

	bodyOut, _ := io.ReadAll(resp.Body)
	var errPayload map[string]any
	if err := json.Unmarshal(bodyOut, &errPayload); err != nil {
		t.Fatalf("decode error response error = %v", err)
	}
	errObj := errPayload["error"].(map[string]any)
	code := errObj["code"].(string)
	if code != "CS_AUTH_4033" {
		t.Fatalf("error code = %s, want CS_AUTH_4033", code)
	}
}

// TestSecurity_InvalidJSONBody verifies that a request with malformed JSON body
// is rejected with 400 and error code CS_REQ_4001.
func TestSecurity_InvalidJSONBody(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	// Malformed JSON — missing closing brace and invalid value
	malformedBody := []byte(`{"message_id":"m-sec-4","channel":"widget","open_id":"u_sec","content":}`)
	timestamp, signature, err := handlers.SignWebhookRequest("e2e-test-secret", time.Now().Unix(), malformedBody)
	if err != nil {
		t.Fatalf("SignWebhookRequest error = %v", err)
	}

	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader(malformedBody))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	req.Header.Set("X-CS-Signature", signature)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}

	bodyOut, _ := io.ReadAll(resp.Body)
	var errPayload map[string]any
	if err := json.Unmarshal(bodyOut, &errPayload); err != nil {
		t.Fatalf("decode error response error = %v", err)
	}
	errObj := errPayload["error"].(map[string]any)
	code := errObj["code"].(string)
	if code != "CS_REQ_4001" {
		t.Fatalf("error code = %s, want CS_REQ_4001", code)
	}
}

// TestSecurity_EmptyBody verifies that a request with an empty body is rejected
// with 400.
func TestSecurity_EmptyBody(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	timestamp, signature, err := handlers.SignWebhookRequest("e2e-test-secret", time.Now().Unix(), []byte{})
	if err != nil {
		t.Fatalf("SignWebhookRequest error = %v", err)
	}

	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader([]byte{}))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	req.Header.Set("X-CS-Signature", signature)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

// TestSecurity_InvalidTimestampFormat verifies that a request with a
// non-numeric timestamp is rejected with 403 and code CS_AUTH_4032.
func TestSecurity_InvalidTimestampFormat(t *testing.T) {
	application := newTestAppWithSecret(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	body := []byte(`{"message_id":"m-sec-5","channel":"widget","open_id":"u_sec","content":"查询额度"}`)
	timestamp := "not-a-number"
	signature := "somesig"

	req, err := http.NewRequest(http.MethodPost, server.URL+"/api/v1/customer-service/webhook", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request error = %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CS-Timestamp", timestamp)
	req.Header.Set("X-CS-Signature", signature)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("do request error = %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("status = %d, want 403", resp.StatusCode)
	}

	bodyOut, _ := io.ReadAll(resp.Body)
	var errPayload map[string]any
	if err := json.Unmarshal(bodyOut, &errPayload); err != nil {
		t.Fatalf("decode error response error = %v", err)
	}
	errObj := errPayload["error"].(map[string]any)
	code := errObj["code"].(string)
	if code != "CS_AUTH_4032" {
		t.Fatalf("error code = %s, want CS_AUTH_4032", code)
	}
}
