//go:build goolm || libolm

package matrix

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// makeTestClient construye un *mautrix.Client apuntando al servidor dado con
// credenciales validas para los tests.
func makeTestClient(t *testing.T, serverURL string) *mautrix.Client {
	t.Helper()
	cli, err := mautrix.NewClient(serverURL, "@user:localhost", "test-token")
	if err != nil {
		t.Fatalf("mautrix.NewClient: %v", err)
	}
	cli.AccessToken = "test-token"
	cli.UserID = id.UserID("@user:localhost")
	cli.DeviceID = id.DeviceID("TESTDEVICE")
	return cli
}

// validPickleKey genera una clave de 32 bytes para tests.
func validPickleKey() []byte {
	key := make([]byte, 32)
	for i := range key {
		key[i] = byte(i + 1)
	}
	return key
}

// newSynapseMock crea un httptest.Server que responde a los endpoints
// necesarios para Init(): /keys/upload y /keys/query.
// Acepta un statusCode para /keys/upload (200 = exito, 401 = token invalido).
func newSynapseMock(t *testing.T, uploadStatus int) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()

	// POST /_matrix/client/v3/keys/upload -> one-time key counts
	mux.HandleFunc("/_matrix/client/v3/keys/upload", func(w http.ResponseWriter, r *http.Request) {
		if uploadStatus != http.StatusOK {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(uploadStatus)
			resp := map[string]any{
				"errcode": "M_UNKNOWN_TOKEN",
				"error":   "Invalid access token",
			}
			_ = json.NewEncoder(w).Encode(resp)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		resp := map[string]any{
			"one_time_key_counts": map[string]int{
				"signed_curve25519": 50,
			},
		}
		_ = json.NewEncoder(w).Encode(resp)
	})

	// POST /_matrix/client/v3/keys/query -> empty device keys
	mux.HandleFunc("/_matrix/client/v3/keys/query", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		resp := map[string]any{
			"device_keys":       map[string]any{},
			"failures":          map[string]any{},
			"master_keys":       map[string]any{},
			"user_signing_keys": map[string]any{},
			"self_signing_keys": map[string]any{},
		}
		_ = json.NewEncoder(w).Encode(resp)
	})

	// GET /_matrix/client/v3/sync -> minimal empty sync
	mux.HandleFunc("/_matrix/client/v3/sync", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		resp := map[string]any{
			"next_batch":                 "s0_1",
			"rooms":                      map[string]any{},
			"to_device":                  map[string]any{"events": []any{}},
			"device_one_time_keys_count": map[string]any{},
		}
		_ = json.NewEncoder(w).Encode(resp)
	})

	// Catchall para no dejar requests colgados
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{}`))
	})

	return httptest.NewServer(mux)
}

func TestMatrixCryptoInit(t *testing.T) {
	t.Run("Client nil devuelve error", func(t *testing.T) {
		ctx := context.Background()
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    nil,
			StorePath: "/tmp/crypto_test.db",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con Client nil, got nil")
		}
		if !strings.Contains(err.Error(), "Client no puede ser nil") {
			t.Errorf("mensaje de error inesperado: %q", err.Error())
		}
	})

	t.Run("AccessToken vacio devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "")
		cli.UserID = "@user:localhost"
		cli.DeviceID = "DEVID"
		cli.AccessToken = ""
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "/tmp/crypto_test.db",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con AccessToken vacio, got nil")
		}
	})

	t.Run("UserID vacio devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "", "token_abc")
		cli.DeviceID = "DEVID"
		cli.AccessToken = "token_abc"
		cli.UserID = ""
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "/tmp/crypto_test.db",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con UserID vacio, got nil")
		}
	})

	t.Run("DeviceID vacio devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token_abc")
		cli.AccessToken = "token_abc"
		cli.UserID = "@user:localhost"
		cli.DeviceID = ""
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "/tmp/crypto_test.db",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con DeviceID vacio, got nil")
		}
	})

	t.Run("StorePath vacio devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
		cli.AccessToken = "token"
		cli.UserID = "@user:localhost"
		cli.DeviceID = id.DeviceID("DEVID")
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con StorePath vacio, got nil")
		}
	})

	t.Run("StorePath relativo devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
		cli.AccessToken = "token"
		cli.UserID = "@user:localhost"
		cli.DeviceID = id.DeviceID("DEVID")
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "relative/path/crypto.db",
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con StorePath relativo, got nil")
		}
	})

	t.Run("PickleKey != 32 bytes devuelve error", func(t *testing.T) {
		ctx := context.Background()
		cli, _ := mautrix.NewClient("http://localhost:8008", "@user:localhost", "token")
		cli.AccessToken = "token"
		cli.UserID = "@user:localhost"
		cli.DeviceID = id.DeviceID("DEVID")
		// Clave de 16 bytes (demasiado corta)
		shortKey := make([]byte, 16)
		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: "/tmp/crypto_test.db",
			PickleKey: shortKey,
		})
		if err == nil {
			t.Fatal("esperaba error con PickleKey de 16 bytes, got nil")
		}
		if !strings.Contains(err.Error(), "32 bytes") {
			t.Errorf("mensaje de error debe mencionar '32 bytes', got %q", err.Error())
		}
	})

	t.Run("directorio del store se crea con permisos 0700", func(t *testing.T) {
		tmpDir := t.TempDir()
		storeDir := filepath.Join(tmpDir, "sub", "crypto_store")
		storePath := filepath.Join(storeDir, "crypto.db")

		srv := newSynapseMock(t, http.StatusOK)
		defer srv.Close()

		cli := makeTestClient(t, srv.URL)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		// El Init puede fallar (e.g. sync loop), pero el directorio debe crearse.
		_, _ = MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: storePath,
			PickleKey: validPickleKey(),
		})

		if _, statErr := os.Stat(storeDir); os.IsNotExist(statErr) {
			t.Fatalf("el directorio %q no fue creado", storeDir)
		}
		info, statErr := os.Stat(storeDir)
		if statErr != nil {
			t.Fatalf("no se pudo stat el directorio: %v", statErr)
		}
		perm := info.Mode().Perm()
		if perm != 0700 {
			t.Errorf("permisos del directorio: got %04o, want 0700", perm)
		}
	})

	t.Run("input valido Init exito helper no nil", func(t *testing.T) {
		tmpDir := t.TempDir()
		storePath := filepath.Join(tmpDir, "crypto.db")

		srv := newSynapseMock(t, http.StatusOK)
		defer srv.Close()

		cli := makeTestClient(t, srv.URL)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		res, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: storePath,
			PickleKey: validPickleKey(),
		})
		if err != nil {
			t.Fatalf("MatrixCryptoInit failed: %v", err)
		}
		if res == nil {
			t.Fatal("resultado es nil")
		}
		if res.Helper == nil {
			t.Fatal("Helper es nil")
		}
		if res.StorePath != storePath {
			t.Errorf("StorePath: got %q, want %q", res.StorePath, storePath)
		}
		if cli.Crypto == nil {
			t.Error("client.Crypto no fue asignado")
		}
		// Verificar que el archivo SQLite fue creado
		if _, err := os.Stat(storePath); os.IsNotExist(err) {
			t.Error("archivo crypto.db no fue creado")
		}
		if err := res.Helper.Close(); err != nil {
			t.Errorf("Helper.Close() error: %v", err)
		}
	})

	t.Run("Synapse 401 en keys upload devuelve error", func(t *testing.T) {
		tmpDir := t.TempDir()
		storePath := filepath.Join(tmpDir, "crypto.db")

		srv := newSynapseMock(t, http.StatusUnauthorized)
		defer srv.Close()

		cli := makeTestClient(t, srv.URL)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		_, err := MatrixCryptoInit(ctx, MatrixCryptoInitConfig{
			Client:    cli,
			StorePath: storePath,
			PickleKey: validPickleKey(),
		})
		if err == nil {
			t.Fatal("esperaba error con Synapse 401, got nil")
		}
		if !strings.Contains(err.Error(), "helper.Init failed") {
			t.Errorf("mensaje de error inesperado: %q", err.Error())
		}
	})
}
