package infra

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/id"
)

// newMXTestClient construye un *mautrix.Client apuntando al servidor httptest dado.
func newMXTestClient(t *testing.T, serverURL string) *mautrix.Client {
	t.Helper()
	cli, err := mautrix.NewClient(serverURL, "@testuser:example.com", "mxat_test_token")
	if err != nil {
		t.Fatalf("newMXTestClient: %v", err)
	}
	cli.DeviceID = id.DeviceID("TESTDEVICE01")
	return cli
}

// mxSendHandler devuelve un http.Handler que:
//   - Acepta PUT /…/rooms/{roomID}/send/{eventType}/{txnID}
//   - Devuelve {"event_id": "$fakeEvent123:example.com"} con 200
//   - Guarda el body JSON decodificado en bodyOut y la path en pathOut para assertions
func mxSendHandler(t *testing.T, bodyOut *map[string]interface{}, pathOut *string) http.Handler {
	t.Helper()
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if pathOut != nil {
			*pathOut = r.URL.Path
		}
		if r.Method != http.MethodPut {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		if bodyOut != nil {
			var parsed map[string]interface{}
			if err := json.NewDecoder(r.Body).Decode(&parsed); err != nil {
				t.Errorf("mxSendHandler: json decode: %v", err)
			}
			*bodyOut = parsed
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"event_id":"$fakeEvent123:example.com"}`))
	})
}

func TestMatrixMessageSend(t *testing.T) {
	ctx := context.Background()
	const roomID = "!testroom:example.com"
	const wantEventID = "$fakeEvent123:example.com"

	t.Run("SendText body correcto y EventID parseado", func(t *testing.T) {
		var body map[string]interface{}
		srv := httptest.NewServer(mxSendHandler(t, &body, nil))
		defer srv.Close()

		cli := newMXTestClient(t, srv.URL)
		evID, err := MatrixSendText(ctx, cli, id.RoomID(roomID), "Hola mundo")
		if err != nil {
			t.Fatalf("MatrixSendText error: %v", err)
		}
		if string(evID) != wantEventID {
			t.Errorf("EventID: got %q, want %q", evID, wantEventID)
		}
		if got := body["msgtype"]; got != "m.text" {
			t.Errorf("body['msgtype']: got %v, want 'm.text'", got)
		}
		if got := body["body"]; got != "Hola mundo" {
			t.Errorf("body['body']: got %v, want 'Hola mundo'", got)
		}
	})

	t.Run("SendMarkdown bold convierte a HTML strong y sanitiza script", func(t *testing.T) {
		var body map[string]interface{}
		srv := httptest.NewServer(mxSendHandler(t, &body, nil))
		defer srv.Close()

		cli := newMXTestClient(t, srv.URL)
		evID, err := MatrixSendMarkdown(ctx, cli, id.RoomID(roomID), "**bold**")
		if err != nil {
			t.Fatalf("MatrixSendMarkdown error: %v", err)
		}
		if string(evID) != wantEventID {
			t.Errorf("EventID: got %q, want %q", evID, wantEventID)
		}
		// Body debe ser el markdown original como fallback
		if got := body["body"]; got != "**bold**" {
			t.Errorf("body['body'] fallback: got %v, want '**bold**'", got)
		}
		// formatted_body debe contener <strong>bold</strong>
		fmtBody, ok := body["formatted_body"].(string)
		if !ok {
			t.Fatalf("formatted_body no es string: %v", body["formatted_body"])
		}
		if !strings.Contains(fmtBody, "<strong>bold</strong>") {
			t.Errorf("formatted_body no contiene <strong>bold</strong>, got: %q", fmtBody)
		}
		// format debe ser org.matrix.custom.html
		if got := body["format"]; got != "org.matrix.custom.html" {
			t.Errorf("format: got %v, want 'org.matrix.custom.html'", got)
		}

		// Sub-test: sanitizer elimina <script>
		const xssPayload = `texto <script>alert(1)</script> seguro`
		var body2 map[string]interface{}
		srv2 := httptest.NewServer(mxSendHandler(t, &body2, nil))
		defer srv2.Close()
		cli2 := newMXTestClient(t, srv2.URL)
		_, err = MatrixSendMarkdown(ctx, cli2, id.RoomID(roomID), xssPayload)
		if err != nil {
			t.Fatalf("MatrixSendMarkdown XSS error: %v", err)
		}
		fmtBody2, ok := body2["formatted_body"].(string)
		if !ok {
			t.Fatalf("formatted_body no es string (XSS test): %v", body2["formatted_body"])
		}
		// El sanitizer debe eliminar el tag <script>...</script> completo.
		// goldmark convierte inline HTML a texto plano antes de sanitizar,
		// por lo que el texto interior puede quedar como texto plano — eso es correcto.
		if strings.Contains(fmtBody2, "<script>") {
			t.Errorf("formatted_body contiene <script> — sanitizer no funciono: %q", fmtBody2)
		}
		if strings.Contains(fmtBody2, "</script>") {
			t.Errorf("formatted_body contiene </script> — sanitizer no funciono: %q", fmtBody2)
		}
	})

	t.Run("SendReply m.relates_to m.in_reply_to presente", func(t *testing.T) {
		var body map[string]interface{}
		srv := httptest.NewServer(mxSendHandler(t, &body, nil))
		defer srv.Close()

		cli := newMXTestClient(t, srv.URL)
		const parentID = "$parentEvent:example.com"
		evID, err := MatrixSendReply(ctx, cli, id.RoomID(roomID), id.EventID(parentID), "ack")
		if err != nil {
			t.Fatalf("MatrixSendReply error: %v", err)
		}
		if string(evID) != wantEventID {
			t.Errorf("EventID: got %q, want %q", evID, wantEventID)
		}
		if got := body["body"]; got != "ack" {
			t.Errorf("body['body']: got %v, want 'ack'", got)
		}
		relatesTo, ok := body["m.relates_to"].(map[string]interface{})
		if !ok {
			t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
		}
		inReplyTo, ok := relatesTo["m.in_reply_to"].(map[string]interface{})
		if !ok {
			t.Fatalf("m.in_reply_to no es object, got: %v", relatesTo["m.in_reply_to"])
		}
		if got := inReplyTo["event_id"]; got != parentID {
			t.Errorf("m.in_reply_to.event_id: got %v, want %q", got, parentID)
		}
	})

	t.Run("EditMessage rel_type m.replace y m.new_content", func(t *testing.T) {
		var body map[string]interface{}
		srv := httptest.NewServer(mxSendHandler(t, &body, nil))
		defer srv.Close()

		cli := newMXTestClient(t, srv.URL)
		const originalID = "$originalEvent:example.com"
		evID, err := MatrixEditMessage(ctx, cli, id.RoomID(roomID), id.EventID(originalID), "texto editado")
		if err != nil {
			t.Fatalf("MatrixEditMessage error: %v", err)
		}
		if string(evID) != wantEventID {
			t.Errorf("EventID: got %q, want %q", evID, wantEventID)
		}
		// fallback body
		if got := body["body"]; got != "* texto editado" {
			t.Errorf("body['body'] fallback: got %v, want '* texto editado'", got)
		}
		newContent, ok := body["m.new_content"].(map[string]interface{})
		if !ok {
			t.Fatalf("m.new_content no es object, got: %v", body["m.new_content"])
		}
		if got := newContent["body"]; got != "texto editado" {
			t.Errorf("m.new_content.body: got %v, want 'texto editado'", got)
		}
		relatesTo, ok := body["m.relates_to"].(map[string]interface{})
		if !ok {
			t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
		}
		if got := relatesTo["rel_type"]; got != "m.replace" {
			t.Errorf("m.relates_to.rel_type: got %v, want 'm.replace'", got)
		}
		if got := relatesTo["event_id"]; got != originalID {
			t.Errorf("m.relates_to.event_id: got %v, want %q", got, originalID)
		}
	})

	t.Run("SendReaction tipo m.reaction con m.annotation y key", func(t *testing.T) {
		var body map[string]interface{}
		var capturedPath string
		srv := httptest.NewServer(mxSendHandler(t, &body, &capturedPath))
		defer srv.Close()

		cli := newMXTestClient(t, srv.URL)
		const targetID = "$targetEvent:example.com"
		evID, err := MatrixSendReaction(ctx, cli, id.RoomID(roomID), id.EventID(targetID), "👍")
		if err != nil {
			t.Fatalf("MatrixSendReaction error: %v", err)
		}
		if string(evID) != wantEventID {
			t.Errorf("EventID: got %q, want %q", evID, wantEventID)
		}
		// URL debe contener "m.reaction"
		if !strings.Contains(capturedPath, "m.reaction") {
			t.Errorf("URL path no contiene 'm.reaction': %q", capturedPath)
		}
		relatesTo, ok := body["m.relates_to"].(map[string]interface{})
		if !ok {
			t.Fatalf("m.relates_to no es object, got: %v", body["m.relates_to"])
		}
		if got := relatesTo["rel_type"]; got != "m.annotation" {
			t.Errorf("m.relates_to.rel_type: got %v, want 'm.annotation'", got)
		}
		if got := relatesTo["key"]; got != "👍" {
			t.Errorf("m.relates_to.key: got %v, want '👍'", got)
		}
		if got := relatesTo["event_id"]; got != targetID {
			t.Errorf("m.relates_to.event_id: got %v, want %q", got, targetID)
		}
	})

	t.Run("SendText client nil devuelve error", func(t *testing.T) {
		_, err := MatrixSendText(ctx, nil, id.RoomID(roomID), "texto")
		if err == nil {
			t.Fatal("esperaba error con client nil, got nil")
		}
	})

	t.Run("SendMarkdown client nil devuelve error", func(t *testing.T) {
		_, err := MatrixSendMarkdown(ctx, nil, id.RoomID(roomID), "**md**")
		if err == nil {
			t.Fatal("esperaba error con client nil, got nil")
		}
	})

	t.Run("SendReply client nil devuelve error", func(t *testing.T) {
		_, err := MatrixSendReply(ctx, nil, id.RoomID(roomID), "$evID:x", "reply")
		if err == nil {
			t.Fatal("esperaba error con client nil, got nil")
		}
	})

	t.Run("EditMessage client nil devuelve error", func(t *testing.T) {
		_, err := MatrixEditMessage(ctx, nil, id.RoomID(roomID), "$evID:x", "new")
		if err == nil {
			t.Fatal("esperaba error con client nil, got nil")
		}
	})

	t.Run("SendReaction client nil devuelve error", func(t *testing.T) {
		_, err := MatrixSendReaction(ctx, nil, id.RoomID(roomID), "$evID:x", "👍")
		if err == nil {
			t.Fatal("esperaba error con client nil, got nil")
		}
	})
}
