//go:build integration && goolm
// +build integration,goolm

// Integration tests against a real Synapse + MAS. Build tag `integration` keeps
// them out of the default `go test ./...` run; `goolm` matches the runtime
// build tag the app uses (cryptohelper backend). All tests skip cleanly when
// MATRIX_INTEGRATION_* env vars are absent so CI never breaks unattended.
//
// Run with:
//
//   MATRIX_INTEGRATION_TOKEN=syt_... \
//   MATRIX_INTEGRATION_USER=@egutierrez:matrix-af2f3d.organic-machine.com \
//   MATRIX_INTEGRATION_DEVICE=DEVICEID \
//   MATRIX_INTEGRATION_TEST_ROOM='!abcd:matrix-af2f3d.organic-machine.com' \
//   go test -tags 'integration goolm' -timeout 120s -v \
//     ./projects/element_agents/apps/matrix_client_pc/...
//
// MATRIX_INTEGRATION_TEST_ROOM is optional — without it the round-trip test
// auto-skips. MATRIX_INTEGRATION_DEVICE is also optional; if missing the test
// calls /whoami to discover the device_id, mirroring what the app does.

package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"fn-registry/projects/element_agents/apps/matrix_client_pc/internal/infra"

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

const integrationHomeserverURL = "https://matrix-af2f3d.organic-machine.com"

// integrationEnv reads the required env vars + returns them. If MATRIX_INTEGRATION_TOKEN
// or MATRIX_INTEGRATION_USER is missing the test should t.Skip — that is the
// canonical "no creds available" signal.
type integrationEnv struct {
	Token    string
	User     string
	Device   string
	TestRoom string
}

func readIntegrationEnv(t *testing.T) integrationEnv {
	t.Helper()
	env := integrationEnv{
		Token:    os.Getenv("MATRIX_INTEGRATION_TOKEN"),
		User:     os.Getenv("MATRIX_INTEGRATION_USER"),
		Device:   os.Getenv("MATRIX_INTEGRATION_DEVICE"),
		TestRoom: os.Getenv("MATRIX_INTEGRATION_TEST_ROOM"),
	}
	if env.Token == "" || env.User == "" {
		t.Skipf("skipping integration test: set MATRIX_INTEGRATION_TOKEN + MATRIX_INTEGRATION_USER to run (got token=%v user=%v)",
			env.Token != "", env.User != "")
	}
	return env
}

// buildService wires up a MatrixService against the real Synapse, persisting
// the test token in a throwaway keyring namespace so we don't pollute the user's
// production credentials. It returns the service + a cleanup that calls Stop.
func buildService(t *testing.T, env integrationEnv) (*MatrixService, func()) {
	t.Helper()

	tmpDir := t.TempDir()
	// Override user config dir so userStoreDir() lands inside the tmpDir
	// instead of clobbering ~/.config/matrix_client_pc/<user>.
	t.Setenv("XDG_CONFIG_HOME", tmpDir)

	// Use a test-scoped keyring service to keep production creds untouched.
	testKeyringService := fmt.Sprintf("fn_registry.matrix_client_pc.test.%d", time.Now().UnixNano())
	s := &MatrixService{
		store: infra.NewKeyringTokenStore(testKeyringService),
	}
	s.SetContext(context.Background())

	// Persist the token + pickle key under the test keyring so Start() can load
	// them the same way the production code does.
	pickle := make([]byte, 32)
	if _, err := rand.Read(pickle); err != nil {
		t.Fatalf("rand: %v", err)
	}
	tok := infra.Token{
		AccessToken:   env.Token,
		UserID:        env.User,
		DeviceID:      env.Device,
		HomeserverURL: integrationHomeserverURL,
		PickleKeyHex:  hex.EncodeToString(pickle),
	}
	if err := s.store.Save(env.User, tok); err != nil {
		t.Skipf("keyring unavailable (likely no D-Bus session): %v", err)
	}

	cleanup := func() {
		s.Stop()
		_ = s.store.Delete(env.User)
		// Belt-and-braces: blow away any crypto.db files left behind.
		_ = os.RemoveAll(filepath.Join(tmpDir, "matrix_client_pc"))
	}
	return s, cleanup
}

func TestIntegration_StartAndListRooms(t *testing.T) {
	env := readIntegrationEnv(t)
	s, cleanup := buildService(t, env)
	defer cleanup()

	if err := s.Start(env.User); err != nil {
		t.Fatalf("Start: %v", err)
	}

	rooms, err := s.ListRooms()
	if err != nil {
		t.Fatalf("ListRooms: %v", err)
	}
	if len(rooms) == 0 {
		t.Fatalf("ListRooms returned 0 rooms — user has no joined rooms? unexpected for an integration test account")
	}
	t.Logf("ListRooms OK: %d rooms (first=%s)", len(rooms), rooms[0].RoomID)
}

func TestIntegration_GetDiagnostics(t *testing.T) {
	env := readIntegrationEnv(t)
	s, cleanup := buildService(t, env)
	defer cleanup()

	if err := s.Start(env.User); err != nil {
		t.Fatalf("Start: %v", err)
	}

	d := s.GetDiagnostics()
	if !d.Started {
		t.Errorf("Diagnostics.Started=false (want true)")
	}
	if !d.ClientReady {
		t.Errorf("Diagnostics.ClientReady=false (want true)")
	}
	if !d.SyncActive {
		t.Errorf("Diagnostics.SyncActive=false (want true)")
	}
	if d.HomeserverURL == "" {
		t.Errorf("Diagnostics.HomeserverURL empty")
	}
	if d.RoomsCount < 1 {
		t.Errorf("Diagnostics.RoomsCount=%d (want >=1)", d.RoomsCount)
	}
	if d.LastError != "" {
		t.Errorf("Diagnostics.LastError=%q (want empty)", d.LastError)
	}
	t.Logf("Diagnostics OK: rooms=%d encrypted=%d dms=%d", d.RoomsCount, d.EncryptedRooms, d.DMsCount)
}

func TestIntegration_RoundtripMessage(t *testing.T) {
	env := readIntegrationEnv(t)
	if env.TestRoom == "" {
		t.Skip("set MATRIX_INTEGRATION_TEST_ROOM=!abcd:server to enable the send/recv round-trip")
	}
	s, cleanup := buildService(t, env)
	defer cleanup()

	if err := s.Start(env.User); err != nil {
		t.Fatalf("Start: %v", err)
	}

	body := fmt.Sprintf("e2e test %d", time.Now().UnixNano())
	evID, err := s.SendText(env.TestRoom, body)
	if err != nil {
		t.Fatalf("SendText: %v", err)
	}
	t.Logf("SendText OK: event_id=%s body=%q", evID, body)

	// Poll LoadTimeline up to 10s waiting for our event to appear.
	deadline := time.Now().Add(10 * time.Second)
	for {
		msgs, err := s.LoadTimeline(env.TestRoom, 20)
		if err != nil {
			t.Fatalf("LoadTimeline: %v", err)
		}
		for _, m := range msgs {
			if m.EventID == evID {
				if m.Body != body {
					// In an encrypted room the body may come back blank (EncryptedRaw=true)
					// — still counts as round-trip success: the event landed.
					if !m.EncryptedRaw {
						t.Errorf("event body mismatch: got %q want %q", m.Body, body)
					}
				}
				t.Logf("Round-trip OK: event_id=%s found after %v", evID, time.Since(deadline.Add(-10*time.Second)))
				return
			}
		}
		if time.Now().After(deadline) {
			t.Fatalf("event %s never appeared in LoadTimeline after 10s", evID)
		}
		time.Sleep(500 * time.Millisecond)
	}
}

// Sanity check that the homeserver is reachable from the test host before we
// burn the rest of the test budget on keyring + sync setup. Skips if env is
// missing so it doesn't run silently in CI either.
func TestIntegration_HomeserverReachable(t *testing.T) {
	env := readIntegrationEnv(t)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	uid, did, err := whoami(ctx, integrationHomeserverURL, env.Token)
	if err != nil {
		t.Fatalf("whoami(%s): %v", integrationHomeserverURL, err)
	}
	if uid != env.User {
		t.Errorf("whoami user_id=%q want %q", uid, env.User)
	}
	// device_id may be empty when MAS-issued tokens lack device binding —
	// don't fail on it, just log.
	t.Logf("whoami OK: user=%s device=%s", uid, did)
	_ = id.UserID(uid)
}
