package e2e

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

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

// newTestAppE2E creates a fully-wired app instance with in-memory stores
// for end-to-end testing.
func newTestAppE2E(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.Runtime.Env = "test"
	application, err := app.New(cfg, logging.New())
	if err != nil {
		t.Fatalf("app.New() error = %v", err)
	}
	return application
}

// webhookResponse mirrors the JSON shape returned by the webhook handler.
type webhookResponse struct {
	Handoff   bool   `json:"handoff"`
	TicketID  string `json:"ticket_id"`
	SessionID string `json:"session_id"`
	Reply     string `json:"reply"`
}

// mustReadBody reads and closes the response body, then decodes JSON into dest.
// On error, calls t.Fatalf.
func mustReadBody(t *testing.T, resp *http.Response, dest any) {
	t.Helper()
	body, err := io.ReadAll(resp.Body)
	resp.Body.Close()
	if err != nil {
		t.Fatalf("read body error = %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200; body: %s", resp.StatusCode, string(body))
	}
	if err := json.Unmarshal(body, dest); err != nil {
		t.Fatalf("decode body error = %v; body: %s", err, string(body))
	}
}

// TestFullTicketFlow_E2E exercises the complete ticket lifecycle:
//   1. Webhook triggers handoff → ticket created
//   2. Ticket is assigned to an agent
//   3. Ticket is resolved by the agent
//   4. Ticket is retrieved and verified in final resolved state
func TestFullTicketFlow_E2E(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	// ── Step 1: Webhook triggers ticket creation ──────────────────────────
	payload := map[string]any{
		"message_id": "m-e2e-1",
		"channel":    "widget",
		"open_id":    "u_e2e_1",
		"content":    "我要申请退款",
	}
	body, _ := json.Marshal(payload)
	webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("webhook POST error = %v", err)
	}
	var whResult webhookResponse
	mustReadBody(t, webhookResp, &whResult)

	if !whResult.Handoff {
		t.Fatalf("[step1] handoff = %v, want true", whResult.Handoff)
	}
	if whResult.TicketID == "" {
		t.Fatalf("[step1] ticket_id is empty, want non-empty")
	}
	if whResult.SessionID == "" {
		t.Fatalf("[step1] session_id is empty, want non-empty")
	}
	ticketID := whResult.TicketID

	// ── Step 2: Assign the ticket to an agent ────────────────────────────
	assignURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/assign?agent_id=agent-e2e-001&actor_id=admin-e2e", baseURL, ticketID)
	assignReq, err := http.NewRequest(http.MethodPost, assignURL, nil)
	if err != nil {
		t.Fatalf("new assign request error = %v", err)
	}
	assignReq.RemoteAddr = "192.168.1.1:12345"
	assignResp, err := http.DefaultClient.Do(assignReq)
	if err != nil {
		t.Fatalf("assign POST error = %v", err)
	}
	assignBody, err := io.ReadAll(assignResp.Body)
	assignResp.Body.Close()
	if err != nil {
		t.Fatalf("read assign body error = %v", err)
	}
	if assignResp.StatusCode != http.StatusOK {
		t.Fatalf("[step2 assign] status = %d, want 200; body: %s", assignResp.StatusCode, string(assignBody))
	}

	var assignPayload map[string]any
	if err := json.Unmarshal(assignBody, &assignPayload); err != nil {
		t.Fatalf("decode assign response error = %v", err)
	}
	if assignPayload["assigned"] != true {
		t.Fatalf("[step2] assigned = %v, want true", assignPayload["assigned"])
	}

	// ── Step 3: Resolve the ticket ────────────────────────────────────────
	resolveURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/resolve?resolution=refund+processed+and+closed&actor_id=agent-e2e-001", baseURL, ticketID)
	resolveReq, err := http.NewRequest(http.MethodPost, resolveURL, nil)
	if err != nil {
		t.Fatalf("new resolve request error = %v", err)
	}
	resolveReq.RemoteAddr = "192.168.1.2:54321"
	resolveResp, err := http.DefaultClient.Do(resolveReq)
	if err != nil {
		t.Fatalf("resolve POST error = %v", err)
	}
	resolveBody, err := io.ReadAll(resolveResp.Body)
	resolveResp.Body.Close()
	if err != nil {
		t.Fatalf("read resolve body error = %v", err)
	}
	if resolveResp.StatusCode != http.StatusOK {
		t.Fatalf("[step3 resolve] status = %d, want 200; body: %s", resolveResp.StatusCode, string(resolveBody))
	}

	var resolvePayload map[string]any
	if err := json.Unmarshal(resolveBody, &resolvePayload); err != nil {
		t.Fatalf("decode resolve response error = %v", err)
	}
	if resolvePayload["resolved"] != true {
		t.Fatalf("[step3] resolved = %v, want true", resolvePayload["resolved"])
	}

	// ── Step 4: Verify ticket is retrievable in final resolved state ──────
	getURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s", baseURL, ticketID)
	getResp, err := http.Get(getURL)
	if err != nil {
		t.Fatalf("GET ticket error = %v", err)
	}
	getBody, err := io.ReadAll(getResp.Body)
	getResp.Body.Close()
	if err != nil {
		t.Fatalf("read GET body error = %v", err)
	}
	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("[step4 get] status = %d, want 200", getResp.StatusCode)
	}

	var ticketPayload map[string]any
	if err := json.Unmarshal(getBody, &ticketPayload); err != nil {
		t.Fatalf("decode ticket response error = %v", err)
	}
	tkt := ticketPayload["ticket"].(map[string]any)
	if tkt["status"] != "resolved" {
		t.Fatalf("[step4] ticket status = %v, want resolved", tkt["status"])
	}
	if tkt["assigned_to"] != "agent-e2e-001" {
		t.Fatalf("[step4] assigned_to = %v, want agent-e2e-001", tkt["assigned_to"])
	}
	if tkt["resolution"] != "refund processed and closed" {
		t.Fatalf("[step4] resolution = %v, want 'refund processed and closed'", tkt["resolution"])
	}
}

// TestFullTicketFlow_AuditLogVerification verifies that each workflow step
// produces a correct final ticket state, proving the audit system wrote
// each transition correctly.
func TestFullTicketFlow_AuditLogVerification(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	// ── Step 1: Create a ticket via webhook ───────────────────────────────
	payload := map[string]any{
		"message_id": "m-audit-1",
		"channel":    "telegram",
		"open_id":    "u_audit_1",
		"content":    "我的账户数据泄露了",
	}
	body, _ := json.Marshal(payload)
	webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("webhook POST error = %v", err)
	}
	var whResult webhookResponse
	mustReadBody(t, webhookResp, &whResult)

	if !whResult.Handoff {
		t.Fatalf("handoff = %v, want true for data-leak intent", whResult.Handoff)
	}
	ticketID := whResult.TicketID

	// ── Step 2: Assign ticket ────────────────────────────────────────────
	assignURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/assign?agent_id=agent-audit-99&actor_id=supervisor-audit", baseURL, ticketID)
	assignReq, _ := http.NewRequest(http.MethodPost, assignURL, nil)
	assignReq.RemoteAddr = "10.0.0.1:11111"
	assignResp, _ := http.DefaultClient.Do(assignReq)
	if assignResp.StatusCode != http.StatusOK {
		t.Fatalf("assign status = %d, want 200", assignResp.StatusCode)
	}
	io.ReadAll(assignResp.Body)
	assignResp.Body.Close()

	// ── Step 3: Resolve ticket ───────────────────────────────────────────
	resolveURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/resolve?resolution=account+secured&actor_id=agent-audit-99", baseURL, ticketID)
	resolveReq, _ := http.NewRequest(http.MethodPost, resolveURL, nil)
	resolveReq.RemoteAddr = "10.0.0.2:22222"
	resolveResp, _ := http.DefaultClient.Do(resolveReq)
	if resolveResp.StatusCode != http.StatusOK {
		t.Fatalf("resolve status = %d, want 200", resolveResp.StatusCode)
	}
	io.ReadAll(resolveResp.Body)
	resolveResp.Body.Close()

	// ── Step 4: Verify final ticket state (audit writes were persisted) ──
	getURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s", baseURL, ticketID)
	getResp, err := http.Get(getURL)
	if err != nil {
		t.Fatalf("GET ticket error = %v", err)
	}
	getBody, err := io.ReadAll(getResp.Body)
	getResp.Body.Close()
	if err != nil {
		t.Fatalf("read GET body error = %v", err)
	}
	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("GET ticket status = %d, want 200", getResp.StatusCode)
	}

	var ticketPayload map[string]any
	if err := json.Unmarshal(getBody, &ticketPayload); err != nil {
		t.Fatalf("decode ticket response error = %v", err)
	}
	tkt := ticketPayload["ticket"].(map[string]any)

	if tkt["status"] != "resolved" {
		t.Fatalf("ticket status = %v, want resolved", tkt["status"])
	}
	if tkt["priority"] != "P1" {
		t.Fatalf("ticket priority = %v, want P1", tkt["priority"])
	}
	if tkt["resolved_at"] == nil {
		t.Fatalf("resolved_at is nil, audit write must have set it during resolve")
	}
	if tkt["resolution"] != "account secured" {
		t.Fatalf("resolution = %v, want 'account secured'", tkt["resolution"])
	}
	if tkt["assigned_to"] != "agent-audit-99" {
		t.Fatalf("assigned_to = %v, want agent-audit-99", tkt["assigned_to"])
	}
}

// TestFullTicketFlow_ListEndpoint_ShowsCreatedTicket verifies that after a
// webhook-triggered handoff, the ticket appears in the GET /tickets list.
func TestFullTicketFlow_ListEndpoint_ShowsCreatedTicket(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	// Create a ticket via webhook
	payload := map[string]any{
		"message_id": "m-list-e2e-1",
		"channel":    "widget",
		"open_id":    "u_list_e2e",
		"content":    "转人工客服",
	}
	body, _ := json.Marshal(payload)
	webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("webhook POST error = %v", err)
	}
	var whResult webhookResponse
	mustReadBody(t, webhookResp, &whResult)
	ticketID := whResult.TicketID

	// Verify ticket appears in GET /tickets list
	listResp, err := http.Get(baseURL + "/api/v1/customer-service/tickets")
	if err != nil {
		t.Fatalf("GET tickets list error = %v", err)
	}
	listBody, err := io.ReadAll(listResp.Body)
	listResp.Body.Close()
	if err != nil {
		t.Fatalf("read list body error = %v", err)
	}
	if listResp.StatusCode != http.StatusOK {
		t.Fatalf("GET tickets status = %d, want 200", listResp.StatusCode)
	}

	var listPayload map[string]any
	if err := json.Unmarshal(listBody, &listPayload); err != nil {
		t.Fatalf("decode list response error = %v", err)
	}

	items, ok := listPayload["items"].([]any)
	if !ok {
		t.Fatalf("items field missing or not an array")
	}

	found := false
	for _, item := range items {
		tkt := item.(map[string]any)
		if tkt["id"] == ticketID {
			found = true
			if tkt["status"] != "open" {
				t.Fatalf("newly created ticket status = %v, want open", tkt["status"])
			}
			break
		}
	}
	if !found {
		t.Fatalf("ticket %s not found in list of %d items", ticketID, len(items))
	}
}

// TestFullTicketFlow_MultipleTickets_MaintainedSeparately verifies that concurrent
// tickets maintain independent state through the workflow.
func TestFullTicketFlow_MultipleTickets_MaintainedSeparately(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	type ticketResult struct {
		id     string
		status string
	}

	results := make([]ticketResult, 0, 2)

	for i := 0; i < 2; i++ {
		content := "我要转人工"
		if i == 0 {
			content = "我要退款"
		}
		payload := map[string]any{
			"message_id": fmt.Sprintf("m-multi-%d", i),
			"channel":    "widget",
			"open_id":    fmt.Sprintf("u_multi_%d", i),
			"content":    content,
		}
		body, _ := json.Marshal(payload)
		webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
		if err != nil {
			t.Fatalf("webhook POST error = %v", err)
		}
		var whResult webhookResponse
		whBody, err := io.ReadAll(webhookResp.Body)
		webhookResp.Body.Close()
		if err != nil {
			t.Fatalf("read webhook body error = %v", err)
		}
		if webhookResp.StatusCode != http.StatusOK {
			t.Fatalf("webhook status = %d, want 200; body: %s", webhookResp.StatusCode, string(whBody))
		}
		if err := json.Unmarshal(whBody, &whResult); err != nil {
			t.Fatalf("decode webhook response error = %v", err)
		}
		ticketID := whResult.TicketID

		// Assign only the first ticket
		if i == 0 {
			assignURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/assign?agent_id=agent-only-first", baseURL, ticketID)
			assignResp, err := http.Post(assignURL, "application/octet-stream", nil)
			if err != nil {
				t.Fatalf("assign POST error = %v", err)
			}
			io.ReadAll(assignResp.Body)
			assignResp.Body.Close()
			if assignResp.StatusCode != http.StatusOK {
				t.Fatalf("assign status = %d, want 200", assignResp.StatusCode)
			}
		}

		// Check state
		getURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s", baseURL, ticketID)
		getResp, err := http.Get(getURL)
		if err != nil {
			t.Fatalf("GET ticket error = %v", err)
		}
		getBody, err := io.ReadAll(getResp.Body)
		getResp.Body.Close()
		if err != nil {
			t.Fatalf("read GET body error = %v", err)
		}
		if getResp.StatusCode != http.StatusOK {
			t.Fatalf("GET ticket status = %d, want 200", getResp.StatusCode)
		}

		var ticketPayload map[string]any
		if err := json.Unmarshal(getBody, &ticketPayload); err != nil {
			t.Fatalf("decode ticket response error = %v", err)
		}
		tkt := ticketPayload["ticket"].(map[string]any)
		results = append(results, ticketResult{id: ticketID, status: tkt["status"].(string)})
	}

	if results[0].status != "assigned" {
		t.Fatalf("ticket[0] status = %s, want assigned", results[0].status)
	}
	if results[1].status != "open" {
		t.Fatalf("ticket[1] status = %s, want open", results[1].status)
	}

	if results[0].id == results[1].id {
		t.Fatalf("ticket IDs should be distinct: %s == %s", results[0].id, results[1].id)
	}
}

// TestFullTicketFlow_WebhookAuditEvent verifies that the webhook handoff
// path correctly records the ticket creation and generates a reply.
func TestFullTicketFlow_WebhookAuditEvent(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	payload := map[string]any{
		"message_id": "m-audit-webhook-1",
		"channel":    "widget",
		"open_id":    "u_audit_webhook",
		"content":    "我要退款",
	}
	body, _ := json.Marshal(payload)
	webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("webhook POST error = %v", err)
	}
	var whResult webhookResponse
	mustReadBody(t, webhookResp, &whResult)

	if !whResult.Handoff {
		t.Fatalf("handoff = %v, want true", whResult.Handoff)
	}
	if whResult.TicketID == "" {
		t.Fatalf("ticket_id is empty, want non-empty")
	}
	if whResult.Reply == "" {
		t.Fatalf("reply is empty, want non-empty (audit reply should be generated)")
	}

	// Verify ticket is in open state
	getURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s", baseURL, whResult.TicketID)
	getResp, err := http.Get(getURL)
	if err != nil {
		t.Fatalf("GET ticket error = %v", err)
	}
	getBody, err := io.ReadAll(getResp.Body)
	getResp.Body.Close()
	if err != nil {
		t.Fatalf("read GET body error = %v", err)
	}
	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("GET ticket status = %d, want 200", getResp.StatusCode)
	}

	var ticketPayload map[string]any
	if err := json.Unmarshal(getBody, &ticketPayload); err != nil {
		t.Fatalf("decode ticket response error = %v", err)
	}
	tkt := ticketPayload["ticket"].(map[string]any)
	if tkt["status"] != "open" {
		t.Fatalf("ticket status = %v, want open", tkt["status"])
	}
}

// TestFullTicketFlow_StateTransitionAuditOrder verifies that audit events
// are written in the correct temporal order by checking final state.
func TestFullTicketFlow_StateTransitionAuditOrder(t *testing.T) {
	application := newTestAppE2E(t)
	server := httptest.NewServer(application.Server.Handler)
	defer server.Close()

	baseURL := server.URL

	// Create ticket via webhook
	payload := map[string]any{
		"message_id": "m-order-1",
		"channel":    "widget",
		"open_id":    "u_order",
		"content":    "转人工",
	}
	body, _ := json.Marshal(payload)
	webhookResp, err := http.Post(baseURL+"/api/v1/customer-service/webhook", "application/json", bytes.NewReader(body))
	if err != nil {
		t.Fatalf("webhook POST error = %v", err)
	}
	var whResult webhookResponse
	whBody, err := io.ReadAll(webhookResp.Body)
	webhookResp.Body.Close()
	if err != nil {
		t.Fatalf("read webhook body error = %v", err)
	}
	if webhookResp.StatusCode != http.StatusOK {
		t.Fatalf("webhook status = %d, want 200; body: %s", webhookResp.StatusCode, string(whBody))
	}
	if err := json.Unmarshal(whBody, &whResult); err != nil {
		t.Fatalf("decode webhook response error = %v", err)
	}
	ticketID := whResult.TicketID

	// Assign (audit event: assign)
	assignURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/assign?agent_id=agent-order-1", baseURL, ticketID)
	assignResp, err := http.Post(assignURL, "application/octet-stream", nil)
	if err != nil {
		t.Fatalf("assign POST error = %v", err)
	}
	io.ReadAll(assignResp.Body)
	assignResp.Body.Close()
	if assignResp.StatusCode != http.StatusOK {
		t.Fatalf("assign status = %d, want 200", assignResp.StatusCode)
	}

	// Resolve (audit event: resolve)
	resolveURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s/resolve?resolution=handled", baseURL, ticketID)
	resolveResp, err := http.Post(resolveURL, "application/octet-stream", nil)
	if err != nil {
		t.Fatalf("resolve POST error = %v", err)
	}
	io.ReadAll(resolveResp.Body)
	resolveResp.Body.Close()
	if resolveResp.StatusCode != http.StatusOK {
		t.Fatalf("resolve status = %d, want 200", resolveResp.StatusCode)
	}

	// Final state check: proves all audit writes succeeded in order
	getURL := fmt.Sprintf("%s/api/v1/customer-service/tickets/%s", baseURL, ticketID)
	getResp, err := http.Get(getURL)
	if err != nil {
		t.Fatalf("GET ticket (final) error = %v", err)
	}
	finalBody, err := io.ReadAll(getResp.Body)
	getResp.Body.Close()
	if err != nil {
		t.Fatalf("read GET body error = %v", err)
	}
	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("GET ticket (final) status = %d, want 200", getResp.StatusCode)
	}

	var finalPayload map[string]any
	if err := json.Unmarshal(finalBody, &finalPayload); err != nil {
		t.Fatalf("decode final ticket response error = %v", err)
	}
	tkt := finalPayload["ticket"].(map[string]any)

	if tkt["status"] != "resolved" {
		t.Fatalf("final status = %v, want resolved", tkt["status"])
	}
	if tkt["assigned_to"] != "agent-order-1" {
		t.Fatalf("final assigned_to = %v, want agent-order-1", tkt["assigned_to"])
	}
	if tkt["resolution"] != "handled" {
		t.Fatalf("final resolution = %v, want handled", tkt["resolution"])
	}
}
