package app

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
	"os"
	"testing"
	"time"

	"next.orly.dev/app/config"
	"next.orly.dev/pkg/acl"
	"git.mleku.dev/mleku/nostr/crypto/keys"
	"next.orly.dev/pkg/database"
	"git.mleku.dev/mleku/nostr/encoders/event"
	"git.mleku.dev/mleku/nostr/encoders/hex"
	"git.mleku.dev/mleku/nostr/encoders/tag"
	"next.orly.dev/pkg/protocol/nip43"
	"next.orly.dev/pkg/protocol/publish"
	"git.mleku.dev/mleku/nostr/relayinfo"
)

// newTestListener creates a properly initialized Listener for testing
func newTestListener(server *Server, ctx context.Context) *Listener {
	listener := &Listener{
		Server:         server,
		ctx:            ctx,
		writeChan:      make(chan publish.WriteRequest, 100),
		writeDone:      make(chan struct{}),
		messageQueue:   make(chan messageRequest, 100),
		processingDone: make(chan struct{}),
		subscriptions:  make(map[string]context.CancelFunc),
	}

	// Start write worker and message processor
	go listener.writeWorker()
	go listener.messageProcessor()

	return listener
}

// closeTestListener properly closes a test listener
func closeTestListener(listener *Listener) {
	close(listener.writeChan)
	<-listener.writeDone
	close(listener.messageQueue)
	<-listener.processingDone
}

// setupE2ETest creates a full test server for end-to-end testing
func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) {
	tempDir, err := os.MkdirTemp("", "nip43_e2e_test_*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	db, err := database.New(ctx, cancel, tempDir, "info")
	if err != nil {
		os.RemoveAll(tempDir)
		t.Fatalf("failed to open database: %v", err)
	}

	cfg := &config.C{
		AppName:                "TestRelay",
		NIP43Enabled:           true,
		NIP43PublishEvents:     true,
		NIP43PublishMemberList: true,
		NIP43InviteExpiry:      24 * time.Hour,
		RelayURL:               "wss://test.relay",
		Listen:                 "localhost",
		Port:                   3334,
		ACLMode:                "none",
		AuthRequired:           false,
	}

	// Generate admin keys
	adminSecret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate admin secret: %v", err)
	}
	adminSigner, err := p8k.New()
	if err != nil {
		t.Fatalf("failed to create admin signer: %v", err)
	}
	if err = adminSigner.InitSec(adminSecret); err != nil {
		t.Fatalf("failed to initialize admin signer: %v", err)
	}
	adminPubkey := adminSigner.Pub()

	// Add admin to config for ACL
	cfg.Admins = []string{hex.Enc(adminPubkey)}

	server := &Server{
		Ctx:           ctx,
		Config:        cfg,
		DB:            db,
		publishers:    publish.New(NewPublisher(ctx)),
		Admins:        [][]byte{adminPubkey},
		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
		cfg:           cfg,
		db:            db,
	}

	// Configure ACL registry
	acl.Registry.SetMode(cfg.ACLMode)
	if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
		db.Close()
		os.RemoveAll(tempDir)
		t.Fatalf("failed to configure ACL: %v", err)
	}

	server.mux = http.NewServeMux()

	// Set up HTTP handlers
	server.mux.HandleFunc(
		"/", func(w http.ResponseWriter, r *http.Request) {
			if r.Header.Get("Accept") == "application/nostr+json" {
				server.HandleRelayInfo(w, r)
				return
			}
			http.NotFound(w, r)
		},
	)

	httpServer := httptest.NewServer(server.mux)

	cleanup := func() {
		httpServer.Close()
		db.Close()
		os.RemoveAll(tempDir)
	}

	return server, httpServer, cleanup
}

// TestE2E_RelayInfoIncludesNIP43 tests that NIP-43 is advertised in relay info
func TestE2E_RelayInfoIncludesNIP43(t *testing.T) {
	server, httpServer, cleanup := setupE2ETest(t)
	defer cleanup()

	// Make request to relay info endpoint
	req, err := http.NewRequest("GET", httpServer.URL, nil)
	if err != nil {
		t.Fatalf("failed to create request: %v", err)
	}
	req.Header.Set("Accept", "application/nostr+json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("failed to make request: %v", err)
	}
	defer resp.Body.Close()

	// Parse relay info
	var info relayinfo.T
	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
		t.Fatalf("failed to decode relay info: %v", err)
	}

	// Verify NIP-43 is in supported NIPs
	hasNIP43 := false
	for _, nip := range info.Nips {
		if nip == 43 {
			hasNIP43 = true
			break
		}
	}

	if !hasNIP43 {
		t.Error("NIP-43 not advertised in supported_nips")
	}

	// Verify server name
	if info.Name != server.Config.AppName {
		t.Errorf(
			"wrong relay name: got %s, want %s", info.Name,
			server.Config.AppName,
		)
	}
}

// TestE2E_CompleteJoinFlow tests the complete user join flow
func TestE2E_CompleteJoinFlow(t *testing.T) {
	server, _, cleanup := setupE2ETest(t)
	defer cleanup()

	// Step 1: Admin requests invite code
	adminPubkey := server.Admins[0]
	inviteEvent, err := server.HandleNIP43InviteRequest(adminPubkey)
	if err != nil {
		t.Fatalf("failed to generate invite: %v", err)
	}

	// Extract invite code
	claimTag := inviteEvent.Tags.GetFirst([]byte("claim"))
	if claimTag == nil || claimTag.Len() < 2 {
		t.Fatal("invite event missing claim tag")
	}
	inviteCode := string(claimTag.T[1])

	// Step 2: User creates join request
	userSecret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate user secret: %v", err)
	}
	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
	if err != nil {
		t.Fatalf("failed to get user pubkey: %v", err)
	}
	signer, err := keys.SecretBytesToSigner(userSecret)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	joinEv := event.New()
	joinEv.Kind = nip43.KindJoinRequest
	copy(joinEv.Pubkey, userPubkey)
	joinEv.Tags = tag.NewS()
	joinEv.Tags.Append(tag.NewFromAny("-"))
	joinEv.Tags.Append(tag.NewFromAny("claim", inviteCode))
	joinEv.CreatedAt = time.Now().Unix()
	joinEv.Content = []byte("")
	if err = joinEv.Sign(signer); err != nil {
		t.Fatalf("failed to sign join event: %v", err)
	}

	// Step 3: Process join request
	listener := newTestListener(server, server.Ctx)
	defer closeTestListener(listener)
	err = listener.HandleNIP43JoinRequest(joinEv)
	if err != nil {
		t.Fatalf("failed to handle join request: %v", err)
	}

	// Step 4: Verify membership
	isMember, err := server.DB.IsNIP43Member(userPubkey)
	if err != nil {
		t.Fatalf("failed to check membership: %v", err)
	}
	if !isMember {
		t.Error("user was not added as member")
	}

	membership, err := server.DB.GetNIP43Membership(userPubkey)
	if err != nil {
		t.Fatalf("failed to get membership: %v", err)
	}
	if membership.InviteCode != inviteCode {
		t.Errorf(
			"wrong invite code: got %s, want %s", membership.InviteCode,
			inviteCode,
		)
	}
}

// TestE2E_InviteCodeReuse tests that invite codes can only be used once
func TestE2E_InviteCodeReuse(t *testing.T) {
	server, _, cleanup := setupE2ETest(t)
	defer cleanup()

	// Generate invite code
	code, err := server.InviteManager.GenerateCode()
	if err != nil {
		t.Fatalf("failed to generate invite code: %v", err)
	}

	listener := newTestListener(server, server.Ctx)
	defer closeTestListener(listener)

	// First user uses the code
	user1Secret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate user1 secret: %v", err)
	}
	user1Pubkey, err := keys.SecretBytesToPubKeyBytes(user1Secret)
	if err != nil {
		t.Fatalf("failed to get user1 pubkey: %v", err)
	}
	signer1, err := keys.SecretBytesToSigner(user1Secret)
	if err != nil {
		t.Fatalf("failed to create signer1: %v", err)
	}

	joinEv1 := event.New()
	joinEv1.Kind = nip43.KindJoinRequest
	copy(joinEv1.Pubkey, user1Pubkey)
	joinEv1.Tags = tag.NewS()
	joinEv1.Tags.Append(tag.NewFromAny("-"))
	joinEv1.Tags.Append(tag.NewFromAny("claim", code))
	joinEv1.CreatedAt = time.Now().Unix()
	joinEv1.Content = []byte("")
	if err = joinEv1.Sign(signer1); err != nil {
		t.Fatalf("failed to sign join event 1: %v", err)
	}

	err = listener.HandleNIP43JoinRequest(joinEv1)
	if err != nil {
		t.Fatalf("failed to handle join request 1: %v", err)
	}

	// Verify first user is member
	isMember, err := server.DB.IsNIP43Member(user1Pubkey)
	if err != nil {
		t.Fatalf("failed to check user1 membership: %v", err)
	}
	if !isMember {
		t.Error("user1 was not added")
	}

	// Second user tries to use same code
	user2Secret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate user2 secret: %v", err)
	}
	user2Pubkey, err := keys.SecretBytesToPubKeyBytes(user2Secret)
	if err != nil {
		t.Fatalf("failed to get user2 pubkey: %v", err)
	}
	signer2, err := keys.SecretBytesToSigner(user2Secret)
	if err != nil {
		t.Fatalf("failed to create signer2: %v", err)
	}

	joinEv2 := event.New()
	joinEv2.Kind = nip43.KindJoinRequest
	copy(joinEv2.Pubkey, user2Pubkey)
	joinEv2.Tags = tag.NewS()
	joinEv2.Tags.Append(tag.NewFromAny("-"))
	joinEv2.Tags.Append(tag.NewFromAny("claim", code))
	joinEv2.CreatedAt = time.Now().Unix()
	joinEv2.Content = []byte("")
	if err = joinEv2.Sign(signer2); err != nil {
		t.Fatalf("failed to sign join event 2: %v", err)
	}

	// Should handle without error but not add user
	err = listener.HandleNIP43JoinRequest(joinEv2)
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}

	// Verify second user is NOT member
	isMember, err = server.DB.IsNIP43Member(user2Pubkey)
	if err != nil {
		t.Fatalf("failed to check user2 membership: %v", err)
	}
	if isMember {
		t.Error("user2 was incorrectly added with reused code")
	}
}

// TestE2E_MembershipListGeneration tests membership list event generation
func TestE2E_MembershipListGeneration(t *testing.T) {
	server, _, cleanup := setupE2ETest(t)
	defer cleanup()

	listener := newTestListener(server, server.Ctx)
	defer closeTestListener(listener)

	// Add multiple members
	memberCount := 5
	members := make([][]byte, memberCount)

	for i := 0; i < memberCount; i++ {
		userSecret, err := keys.GenerateSecretKey()
		if err != nil {
			t.Fatalf("failed to generate user secret %d: %v", i, err)
		}
		userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
		if err != nil {
			t.Fatalf("failed to get user pubkey %d: %v", i, err)
		}
		members[i] = userPubkey

		// Add directly to database for speed
		err = server.DB.AddNIP43Member(userPubkey, "code")
		if err != nil {
			t.Fatalf("failed to add member %d: %v", i, err)
		}
	}

	// Generate membership list
	err := listener.publishMembershipList()
	if err != nil {
		t.Fatalf("failed to publish membership list: %v", err)
	}

	// Note: In a real test, you would verify the event was published
	// through the publishers system. For now, we just verify no error.
}

// TestE2E_ExpiredInviteCode tests that expired codes are rejected
func TestE2E_ExpiredInviteCode(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "nip43_expired_test_*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	db, err := database.New(ctx, cancel, tempDir, "info")
	if err != nil {
		t.Fatalf("failed to open database: %v", err)
	}
	defer db.Close()

	cfg := &config.C{
		NIP43Enabled:      true,
		NIP43InviteExpiry: 1 * time.Millisecond, // Very short expiry
	}

	server := &Server{
		Ctx:           ctx,
		Config:        cfg,
		DB:            db,
		publishers:    publish.New(NewPublisher(ctx)),
		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
		cfg:           cfg,
		db:            db,
	}

	listener := newTestListener(server, ctx)
	defer closeTestListener(listener)

	// Generate invite code
	code, err := server.InviteManager.GenerateCode()
	if err != nil {
		t.Fatalf("failed to generate invite code: %v", err)
	}

	// Wait for expiry
	time.Sleep(10 * time.Millisecond)

	// Try to use expired code
	userSecret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate user secret: %v", err)
	}
	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
	if err != nil {
		t.Fatalf("failed to get user pubkey: %v", err)
	}
	signer, err := keys.SecretBytesToSigner(userSecret)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	joinEv := event.New()
	joinEv.Kind = nip43.KindJoinRequest
	copy(joinEv.Pubkey, userPubkey)
	joinEv.Tags = tag.NewS()
	joinEv.Tags.Append(tag.NewFromAny("-"))
	joinEv.Tags.Append(tag.NewFromAny("claim", code))
	joinEv.CreatedAt = time.Now().Unix()
	joinEv.Content = []byte("")
	if err = joinEv.Sign(signer); err != nil {
		t.Fatalf("failed to sign event: %v", err)
	}

	err = listener.HandleNIP43JoinRequest(joinEv)
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}

	// Verify user was NOT added
	isMember, err := db.IsNIP43Member(userPubkey)
	if err != nil {
		t.Fatalf("failed to check membership: %v", err)
	}
	if isMember {
		t.Error("user was added with expired code")
	}
}

// TestE2E_InvalidTimestampRejected tests that events with invalid timestamps are rejected
func TestE2E_InvalidTimestampRejected(t *testing.T) {
	server, _, cleanup := setupE2ETest(t)
	defer cleanup()

	listener := newTestListener(server, server.Ctx)
	defer closeTestListener(listener)

	// Generate invite code
	code, err := server.InviteManager.GenerateCode()
	if err != nil {
		t.Fatalf("failed to generate invite code: %v", err)
	}

	// Create user
	userSecret, err := keys.GenerateSecretKey()
	if err != nil {
		t.Fatalf("failed to generate user secret: %v", err)
	}
	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
	if err != nil {
		t.Fatalf("failed to get user pubkey: %v", err)
	}
	signer, err := keys.SecretBytesToSigner(userSecret)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	// Create join request with timestamp far in the past
	joinEv := event.New()
	joinEv.Kind = nip43.KindJoinRequest
	copy(joinEv.Pubkey, userPubkey)
	joinEv.Tags = tag.NewS()
	joinEv.Tags.Append(tag.NewFromAny("-"))
	joinEv.Tags.Append(tag.NewFromAny("claim", code))
	joinEv.CreatedAt = time.Now().Unix() - 700 // More than 10 minutes ago
	joinEv.Content = []byte("")
	if err = joinEv.Sign(signer); err != nil {
		t.Fatalf("failed to sign event: %v", err)
	}

	// Should handle without error but not add user
	err = listener.HandleNIP43JoinRequest(joinEv)
	if err != nil {
		t.Fatalf("handler returned error: %v", err)
	}

	// Verify user was NOT added
	isMember, err := server.DB.IsNIP43Member(userPubkey)
	if err != nil {
		t.Fatalf("failed to check membership: %v", err)
	}
	if isMember {
		t.Error("user was added with invalid timestamp")
	}
}

// BenchmarkJoinRequestProcessing benchmarks join request processing
func BenchmarkJoinRequestProcessing(b *testing.B) {
	tempDir, err := os.MkdirTemp("", "nip43_bench_*")
	if err != nil {
		b.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	db, err := database.New(ctx, cancel, tempDir, "error")
	if err != nil {
		b.Fatalf("failed to open database: %v", err)
	}
	defer db.Close()

	cfg := &config.C{
		NIP43Enabled:      true,
		NIP43InviteExpiry: 24 * time.Hour,
	}

	server := &Server{
		Ctx:           ctx,
		Config:        cfg,
		DB:            db,
		publishers:    publish.New(NewPublisher(ctx)),
		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
		cfg:           cfg,
		db:            db,
	}

	listener := newTestListener(server, ctx)
	defer closeTestListener(listener)

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		// Generate unique user and code for each iteration
		userSecret, _ := keys.GenerateSecretKey()
		userPubkey, _ := keys.SecretBytesToPubKeyBytes(userSecret)
		signer, _ := keys.SecretBytesToSigner(userSecret)
		code, _ := server.InviteManager.GenerateCode()

		joinEv := event.New()
		joinEv.Kind = nip43.KindJoinRequest
		copy(joinEv.Pubkey, userPubkey)
		joinEv.Tags = tag.NewS()
		joinEv.Tags.Append(tag.NewFromAny("-"))
		joinEv.Tags.Append(tag.NewFromAny("claim", code))
		joinEv.CreatedAt = time.Now().Unix()
		joinEv.Content = []byte("")
		joinEv.Sign(signer)

		listener.HandleNIP43JoinRequest(joinEv)
	}
}
