package matrix

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

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

// matrixTestServer simula las respuestas de Synapse para MatrixRoomList.
// Los room IDs contienen '!' que mautrix URL-codifica como %21 en el path;
// los handlers lo decodifican antes de hacer lookup.
type matrixTestServer struct {
	*httptest.Server
	joinedRooms    []string            // room IDs que devuelve /joined_rooms
	roomNames      map[string]string   // roomID -> name (no seteado = sin m.room.name)
	encryptedRooms map[string]bool     // roomID -> tiene encryption event
	directContent  map[string][]string // userID -> []roomID
	roomTags       map[string][]string // roomID -> []tag names
}

func newMatrixTestServer(t *testing.T) *matrixTestServer {
	t.Helper()
	ts := &matrixTestServer{
		joinedRooms:    []string{},
		roomNames:      map[string]string{},
		encryptedRooms: map[string]bool{},
		directContent:  map[string][]string{},
		roomTags:       map[string][]string{},
	}
	mux := http.NewServeMux()

	// GET /_matrix/client/v3/joined_rooms
	mux.HandleFunc("/_matrix/client/v3/joined_rooms", func(w http.ResponseWriter, r *http.Request) {
		rooms := make([]string, len(ts.joinedRooms))
		copy(rooms, ts.joinedRooms)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{"joined_rooms": rooms})
	})

	// Prefix handler para /rooms/ y /user/
	mux.HandleFunc("/_matrix/", func(w http.ResponseWriter, r *http.Request) {
		// URL-decode el path completo para manejar %21 -> !
		rawPath := r.URL.Path
		decodedPath, err := url.PathUnescape(rawPath)
		if err != nil {
			decodedPath = rawPath
		}

		w.Header().Set("Content-Type", "application/json")

		switch {
		// /user/{uid}/account_data/m.direct
		case strings.Contains(decodedPath, "/account_data/m.direct") && strings.Contains(decodedPath, "/user/"):
			json.NewEncoder(w).Encode(ts.directContent)

		// /rooms/{roomId}/state (full state array)
		case strings.Contains(decodedPath, "/rooms/") && strings.HasSuffix(decodedPath, "/state"):
			roomID := extractRoomIDFromPath(decodedPath, "/state")
			ts.serveFullState(w, roomID)

		// /rooms/{roomId}/messages
		case strings.Contains(decodedPath, "/rooms/") && strings.Contains(decodedPath, "/messages"):
			// Devolver chunk vacio para simplificar (LastEventTs = 0)
			json.NewEncoder(w).Encode(map[string]any{
				"chunk": []any{},
				"start": "",
			})

		// /rooms/{roomId}/account_data/m.tag
		case strings.Contains(decodedPath, "/rooms/") && strings.Contains(decodedPath, "/account_data/m.tag"):
			roomID := extractRoomIDFromPath(decodedPath, "/account_data")
			tags, ok := ts.roomTags[roomID]
			if !ok || len(tags) == 0 {
				http.NotFound(w, r)
				return
			}
			tagMap := make(map[string]any)
			for _, tag := range tags {
				tagMap[tag] = map[string]any{}
			}
			json.NewEncoder(w).Encode(map[string]any{"tags": tagMap})

		default:
			http.NotFound(w, r)
		}
	})

	srv := httptest.NewServer(mux)
	ts.Server = srv
	t.Cleanup(srv.Close)
	return ts
}

// extractRoomIDFromPath extrae el roomID de /...rooms/{roomId}/{suffix}.
// suffix debe empezar con "/" (ej. "/state", "/account_data").
func extractRoomIDFromPath(path, suffix string) string {
	// Encontrar el segmento entre /rooms/ y suffix
	roomsIdx := strings.Index(path, "/rooms/")
	if roomsIdx < 0 {
		return ""
	}
	after := path[roomsIdx+len("/rooms/"):]
	suffixIdx := strings.Index(after, suffix)
	if suffixIdx < 0 {
		// suffix no encontrado -> el roomID es lo que queda
		return after
	}
	return after[:suffixIdx]
}

// serveFullState construye y escribe el array de state events para el room.
func (ts *matrixTestServer) serveFullState(w http.ResponseWriter, roomID string) {
	events := []map[string]any{}

	// m.room.name (si existe)
	if name, ok := ts.roomNames[roomID]; ok && name != "" {
		events = append(events, map[string]any{
			"type":      "m.room.name",
			"state_key": "",
			"content":   map[string]any{"name": name},
			"event_id":  "$name",
			"sender":    "@bot:test",
			"room_id":   roomID,
		})
	}

	// m.room.create (sin space)
	events = append(events, map[string]any{
		"type":      "m.room.create",
		"state_key": "",
		"content":   map[string]any{"room_version": "9"},
		"event_id":  "$create",
		"sender":    "@user:test",
		"room_id":   roomID,
	})

	// m.room.member: dos joined members
	events = append(events, map[string]any{
		"type":      "m.room.member",
		"state_key": "@alice:test",
		"content":   map[string]any{"membership": "join", "displayname": "Alice"},
		"event_id":  "$member1",
		"sender":    "@alice:test",
		"room_id":   roomID,
	})
	events = append(events, map[string]any{
		"type":      "m.room.member",
		"state_key": "@bob:test",
		"content":   map[string]any{"membership": "join", "displayname": "Bob"},
		"event_id":  "$member2",
		"sender":    "@bob:test",
		"room_id":   roomID,
	})

	// m.room.encryption (si aplica)
	if ts.encryptedRooms[roomID] {
		events = append(events, map[string]any{
			"type":      "m.room.encryption",
			"state_key": "",
			"content":   map[string]any{"algorithm": "m.megolm.v1.aes-sha2"},
			"event_id":  "$enc",
			"sender":    "@alice:test",
			"room_id":   roomID,
		})
	}

	json.NewEncoder(w).Encode(events)
}

// newTestClient crea un cliente mautrix apuntando al servidor httptest.
func newTestClient(t *testing.T, srv *matrixTestServer) *mautrix.Client {
	t.Helper()
	cli, err := mautrix.NewClient(srv.URL, id.UserID("@user:test"), "test_token")
	if err != nil {
		t.Fatalf("NewClient: %v", err)
	}
	return cli
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

// Test 1: 3 rooms devueltos con metadata correcta.
func TestMatrixRoomList_ThreeRoomsMetadata(t *testing.T) {
	t.Run("3 rooms devueltos con metadata correcta", func(t *testing.T) {
		srv := newMatrixTestServer(t)
		srv.joinedRooms = []string{"!room1:test", "!room2:test", "!room3:test"}
		srv.roomNames = map[string]string{
			"!room1:test": "General",
			"!room2:test": "Engineering",
			"!room3:test": "Random",
		}

		cli := newTestClient(t, srv)
		rooms, err := MatrixRoomList(context.Background(), MatrixRoomListConfig{Client: cli})
		if err != nil {
			t.Fatalf("MatrixRoomList: %v", err)
		}
		if len(rooms) != 3 {
			t.Fatalf("got %d rooms, want 3", len(rooms))
		}

		nameSet := map[string]bool{}
		for _, r := range rooms {
			nameSet[r.Name] = true
			if r.RoomID == "" {
				t.Error("RoomID vacio en algun room")
			}
			// State simulado tiene 2 joined members (alice + bob)
			if r.MemberCount != 2 {
				t.Errorf("room %s: got MemberCount=%d, want 2", r.RoomID, r.MemberCount)
			}
		}
		for _, want := range []string{"General", "Engineering", "Random"} {
			if !nameSet[want] {
				t.Errorf("nombre %q no encontrado en rooms", want)
			}
		}
	})
}

// Test 2: room sin m.room.name -> fallback name no vacio.
func TestMatrixRoomList_FallbackName(t *testing.T) {
	t.Run("1 room sin m.room.name usa fallback name", func(t *testing.T) {
		srv := newMatrixTestServer(t)
		srv.joinedRooms = []string{"!noname:test"}
		// No registramos nombre para !noname:test

		cli := newTestClient(t, srv)
		rooms, err := MatrixRoomList(context.Background(), MatrixRoomListConfig{Client: cli})
		if err != nil {
			t.Fatalf("MatrixRoomList: %v", err)
		}
		if len(rooms) != 1 {
			t.Fatalf("got %d rooms, want 1", len(rooms))
		}
		r := rooms[0]
		if r.Name == "" {
			t.Error("Name no debe ser vacio tras fallback")
		}
		t.Logf("fallback name para room sin m.room.name: %q", r.Name)
	})
}

// Test 3: IsDirect set correctamente segun m.direct.
func TestMatrixRoomList_IsDirect(t *testing.T) {
	t.Run("IsDirect set correctamente segun m.direct", func(t *testing.T) {
		srv := newMatrixTestServer(t)
		srv.joinedRooms = []string{"!dm:test", "!group:test"}
		srv.roomNames = map[string]string{
			"!dm:test":    "Alice DM",
			"!group:test": "Team channel",
		}
		// m.direct: !dm:test es DM con @alice:test
		srv.directContent = map[string][]string{
			"@alice:test": {"!dm:test"},
		}

		cli := newTestClient(t, srv)
		rooms, err := MatrixRoomList(context.Background(), MatrixRoomListConfig{Client: cli})
		if err != nil {
			t.Fatalf("MatrixRoomList: %v", err)
		}
		if len(rooms) != 2 {
			t.Fatalf("got %d rooms, want 2", len(rooms))
		}

		for _, r := range rooms {
			switch r.RoomID {
			case "!dm:test":
				if !r.IsDirect {
					t.Errorf("!dm:test: IsDirect debe ser true")
				}
			case "!group:test":
				if r.IsDirect {
					t.Errorf("!group:test: IsDirect debe ser false")
				}
			}
		}
	})
}

// Test 4: IsEncrypted set segun presencia de m.room.encryption.
func TestMatrixRoomList_IsEncrypted(t *testing.T) {
	t.Run("IsEncrypted set segun presencia de m.room.encryption", func(t *testing.T) {
		srv := newMatrixTestServer(t)
		srv.joinedRooms = []string{"!encrypted:test", "!plain:test"}
		srv.roomNames = map[string]string{
			"!encrypted:test": "Encrypted room",
			"!plain:test":     "Plain room",
		}
		srv.encryptedRooms = map[string]bool{
			"!encrypted:test": true,
		}

		cli := newTestClient(t, srv)
		rooms, err := MatrixRoomList(context.Background(), MatrixRoomListConfig{Client: cli})
		if err != nil {
			t.Fatalf("MatrixRoomList: %v", err)
		}
		if len(rooms) != 2 {
			t.Fatalf("got %d rooms, want 2", len(rooms))
		}

		for _, r := range rooms {
			switch r.RoomID {
			case "!encrypted:test":
				if !r.IsEncrypted {
					t.Errorf("!encrypted:test: IsEncrypted debe ser true")
				}
			case "!plain:test":
				if r.IsEncrypted {
					t.Errorf("!plain:test: IsEncrypted debe ser false")
				}
			}
		}
	})
}

// Test 5: client nil -> error.
func TestMatrixRoomList_NilClient(t *testing.T) {
	t.Run("client nil devuelve error", func(t *testing.T) {
		_, err := MatrixRoomList(context.Background(), MatrixRoomListConfig{Client: nil})
		if err == nil {
			t.Fatal("se esperaba error para client nil, got nil")
		}
		if !strings.Contains(err.Error(), "nil") {
			t.Errorf("el error deberia mencionar nil, got: %v", err)
		}
	})
}
