package infra

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

func newSynapseTestServer(t *testing.T) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()

	// GET /_synapse/admin/v2/users  (list)
	// Note: exact path match (no trailing slash) catches the list endpoint only.
	mux.HandleFunc("/_synapse/admin/v2/users", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		if r.Header.Get("Authorization") == "Bearer bad" {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusForbidden)
			w.Write([]byte(`{"errcode":"M_FORBIDDEN","error":"not admin"}`))
			return
		}
		w.Header().Set("Content-Type", "application/json")
		nextToken := 2
		json.NewEncoder(w).Encode(map[string]interface{}{
			"users": []map[string]interface{}{
				{"name": "@alice:server", "admin": true, "deactivated": false, "creation_ts": 1000},
				{"name": "@bob:server", "admin": false, "deactivated": false, "creation_ts": 2000},
			},
			"total":      2,
			"next_token": nextToken,
		})
	})

	// GET /_synapse/admin/v2/users/{userID}  (single user + devices)
	mux.HandleFunc("/_synapse/admin/v2/users/", func(w http.ResponseWriter, r *http.Request) {
		suffix := strings.TrimPrefix(r.URL.Path, "/_synapse/admin/v2/users/")

		// devices sub-path
		if strings.HasSuffix(suffix, "/devices") {
			if r.Method != http.MethodGet {
				http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
				return
			}
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]interface{}{
				"devices": []map[string]interface{}{
					{"device_id": "AABBCC", "display_name": "Alice's phone", "last_seen_ip": "1.2.3.4", "last_seen_ts": 9999},
					{"device_id": "DDEEFF", "display_name": "Alice's laptop", "last_seen_ip": "5.6.7.8", "last_seen_ts": 8888},
				},
				"total": 2,
			})
			return
		}

		// single device delete sub-path: /{userID}/devices/{deviceID}
		if strings.Contains(suffix, "/devices/") {
			if r.Method != http.MethodDelete {
				http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
				return
			}
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(`{}`))
			return
		}

		// single user GET
		if r.Method != http.MethodGet {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		// 404 for missing user
		if strings.Contains(suffix, "missing") {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusNotFound)
			w.Write([]byte(`{"errcode":"M_NOT_FOUND","error":"User not found"}`))
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(AdminUser{
			UserID:      "@alice:server",
			DisplayName: "Alice",
			Admin:       true,
		})
	})

	// POST /_synapse/admin/v1/deactivate/{userID}
	mux.HandleFunc("/_synapse/admin/v1/deactivate/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		body, _ := io.ReadAll(r.Body)
		var req map[string]interface{}
		if err := json.Unmarshal(body, &req); err != nil {
			http.Error(w, `{"errcode":"M_BAD_JSON","error":"bad json"}`, http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"id_server_unbind_result": "success"})
	})

	// GET /_synapse/admin/v1/rooms  (list)
	mux.HandleFunc("/_synapse/admin/v1/rooms", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"rooms": []map[string]interface{}{
				{"room_id": "!abc:server", "name": "general", "joined_members": 5},
				{"room_id": "!xyz:server", "name": "off-topic", "joined_members": 3},
			},
			"total_rooms": 2,
		})
	})

	// GET /_synapse/admin/v1/rooms/{roomID}
	mux.HandleFunc("/_synapse/admin/v1/rooms/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(AdminRoom{RoomID: "!abc:server", Name: "general", JoinedMembers: 5})
	})

	// DELETE /_synapse/admin/v2/rooms/{roomID}  (async delete)
	mux.HandleFunc("/_synapse/admin/v2/rooms/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodDelete {
			http.Error(w, `{"errcode":"M_UNKNOWN","error":"bad method"}`, http.StatusMethodNotAllowed)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"delete_id": "del_001"})
	})

	return httptest.NewServer(mux)
}

func TestSynapseAdminClient(t *testing.T) {
	srv := newSynapseTestServer(t)
	defer srv.Close()

	cl := NewSynapseAdminClient(srv.URL, "mxat_test_token")
	ctx := context.Background()

	t.Run("ListUsers parses + counts", func(t *testing.T) {
		res, err := cl.ListUsers(ctx, ListUsersFilter{From: 0, Limit: 50})
		if err != nil {
			t.Fatalf("ListUsers: %v", err)
		}
		if res.TotalCount != 2 {
			t.Errorf("TotalCount: got %d, want 2", res.TotalCount)
		}
		if len(res.Users) != 2 {
			t.Fatalf("len(Users): got %d, want 2", len(res.Users))
		}
		if res.Users[0].UserID != "@alice:server" {
			t.Errorf("Users[0].UserID: got %q, want @alice:server", res.Users[0].UserID)
		}
		if !res.Users[0].Admin {
			t.Error("Users[0].Admin should be true")
		}
		if res.NextToken == nil {
			t.Error("NextToken should be non-nil (test server returns next_token=2)")
		} else if *res.NextToken != 2 {
			t.Errorf("NextToken: got %d, want 2", *res.NextToken)
		}
	})

	t.Run("GetUser inexistente -> error contiene M_NOT_FOUND", func(t *testing.T) {
		_, err := cl.GetUser(ctx, "@missing:server")
		if err == nil {
			t.Fatal("expected error, got nil")
		}
		if !strings.Contains(err.Error(), "M_NOT_FOUND") {
			t.Errorf("error should contain M_NOT_FOUND, got: %v", err)
		}
	})

	t.Run("DeactivateUser ok", func(t *testing.T) {
		// Verify via a targeted server that erase=true reaches the body.
		var gotErase bool
		deactivateSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			body, _ := io.ReadAll(r.Body)
			var req map[string]interface{}
			json.Unmarshal(body, &req)
			if v, ok := req["erase"].(bool); ok {
				gotErase = v
			}
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]string{"id_server_unbind_result": "success"})
		}))
		defer deactivateSrv.Close()

		clDe := NewSynapseAdminClient(deactivateSrv.URL, "tok")
		if err := clDe.DeactivateUser(ctx, "@user:server", true); err != nil {
			t.Fatalf("DeactivateUser: %v", err)
		}
		if !gotErase {
			t.Error("erase=true not forwarded in request body")
		}
	})

	t.Run("DeleteRoom devuelve delete_id", func(t *testing.T) {
		var gotPurge, gotBlock bool
		deleteSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Method != http.MethodDelete {
				http.Error(w, `{}`, 405)
				return
			}
			body, _ := io.ReadAll(r.Body)
			var req map[string]interface{}
			json.Unmarshal(body, &req)
			if v, ok := req["purge"].(bool); ok {
				gotPurge = v
			}
			if v, ok := req["block"].(bool); ok {
				gotBlock = v
			}
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]string{"delete_id": "del_007"})
		}))
		defer deleteSrv.Close()

		clDel := NewSynapseAdminClient(deleteSrv.URL, "tok")
		deleteID, err := clDel.DeleteRoom(ctx, "!room:server", "cleanup", true, true)
		if err != nil {
			t.Fatalf("DeleteRoom: %v", err)
		}
		if deleteID != "del_007" {
			t.Errorf("deleteID: got %q, want del_007", deleteID)
		}
		if !gotPurge {
			t.Error("purge=true not forwarded in request body")
		}
		if !gotBlock {
			t.Error("block=true not forwarded in request body")
		}
	})

	t.Run("ListUserDevices parses array", func(t *testing.T) {
		devices, err := cl.ListUserDevices(ctx, "@alice:server")
		if err != nil {
			t.Fatalf("ListUserDevices: %v", err)
		}
		if len(devices) != 2 {
			t.Fatalf("len(devices): got %d, want 2", len(devices))
		}
		if devices[0].DeviceID != "AABBCC" {
			t.Errorf("devices[0].DeviceID: got %q, want AABBCC", devices[0].DeviceID)
		}
		if devices[0].LastSeenIP != "1.2.3.4" {
			t.Errorf("devices[0].LastSeenIP: got %q, want 1.2.3.4", devices[0].LastSeenIP)
		}
	})

	t.Run("HTTP 403 -> error con errcode M_FORBIDDEN", func(t *testing.T) {
		badCl := NewSynapseAdminClient(srv.URL, "bad")
		_, err := badCl.ListUsers(ctx, ListUsersFilter{})
		if err == nil {
			t.Fatal("expected error for 403, got nil")
		}
		if !strings.Contains(err.Error(), "M_FORBIDDEN") {
			t.Errorf("error should contain M_FORBIDDEN, got: %v", err)
		}
	})
}
