// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package rpc

import (
	"context"
	"errors"
	"net/http"
	"testing"
	"time"

	"github.com/luxfi/ids"
	"github.com/luxfi/log"
	"github.com/luxfi/node/server/http"
	"github.com/luxfi/runtime"
	"github.com/luxfi/vm"

	"github.com/stretchr/testify/require"
)

// mockServer implements a test server for handler registration
type mockServer struct {
	routes      map[string]http.Handler
	failCount   int
	maxFailures int
	returnError error
	aliases     map[string][]string
}

func newMockServer() *mockServer {
	return &mockServer{
		routes:      make(map[string]http.Handler),
		maxFailures: 0,
		aliases:     make(map[string][]string),
	}
}

func (s *mockServer) AddRoute(handler http.Handler, base, endpoint string) error {
	// Simulate transient failures for retry testing
	if s.failCount < s.maxFailures {
		s.failCount++
		return errors.New("transient failure")
	}

	// Return configured error if any
	if s.returnError != nil {
		return s.returnError
	}

	// Store the route
	key := base + endpoint
	s.routes[key] = handler
	return nil
}

func (s *mockServer) AddAliases(endpoint string, aliases ...string) error {
	s.aliases[endpoint] = aliases
	return nil
}

func (s *mockServer) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
	return s.AddRoute(handler, base, endpoint)
}

func (s *mockServer) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
	return s.AddAliases(endpoint, aliases...)
}

func (s *mockServer) Dispatch() error { return nil }
func (s *mockServer) RegisterChain(chainName string, rt *runtime.Runtime, vm vm.VM) {
}
func (s *mockServer) Shutdown() error                               { return nil }
func (s *mockServer) SetRootInfoProvider(_ server.RootInfoProvider) {}

func TestHandlerManager_RegisterChainHandlers(t *testing.T) {
	tests := []struct {
		name         string
		chainID      ids.ID
		chainAlias   string
		handlers     map[string]http.Handler
		serverError  error
		expectError  bool
		expectRoutes int
	}{
		{
			name:       "successful registration with alias",
			chainID:    ids.GenerateTestID(),
			chainAlias: "C",
			handlers: map[string]http.Handler{
				"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				}),
				"/ws": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				}),
			},
			expectError:  false,
			expectRoutes: 4, // 2 endpoints × 2 bases (alias + ID)
		},
		{
			name:    "successful registration without alias",
			chainID: ids.GenerateTestID(),
			handlers: map[string]http.Handler{
				"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
				}),
			},
			expectError:  false,
			expectRoutes: 1,
		},
		{
			name:        "nil handler validation",
			chainID:     ids.GenerateTestID(),
			chainAlias:  "X",
			handlers:    map[string]http.Handler{"/rpc": nil},
			expectError: true,
		},
		{
			name:        "empty endpoint validation",
			chainID:     ids.GenerateTestID(),
			handlers:    map[string]http.Handler{"": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
			expectError: true,
		},
		{
			name:        "no handlers provided",
			chainID:     ids.GenerateTestID(),
			handlers:    map[string]http.Handler{},
			expectError: true,
		},
		{
			name:        "invalid endpoint format",
			chainID:     ids.GenerateTestID(),
			handlers:    map[string]http.Handler{"rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Setup
			server := newMockServer()
			server.returnError = tt.serverError
			logger := log.NewNoOpLogger()
			manager := NewHandlerManager(server, logger)

			// Execute
			ctx := context.Background()
			err := manager.RegisterChainHandlers(ctx, tt.chainID, tt.chainAlias, tt.handlers)

			// Verify
			if tt.expectError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.Len(t, server.routes, tt.expectRoutes)

				// Verify route info was stored
				info, exists := manager.GetRouteInfo(tt.chainID)
				require.True(t, exists)
				require.Equal(t, tt.chainID, info.ChainID)
				require.Equal(t, tt.chainAlias, info.ChainAlias)
			}
		})
	}
}

func TestHandlerManager_RetryLogic(t *testing.T) {
	// Setup server that fails twice then succeeds
	server := newMockServer()
	server.maxFailures = 2

	logger := log.NewNoOpLogger()
	manager := NewHandlerManager(server, logger)
	manager.SetRetryConfig(3, 10*time.Millisecond)

	// Create test handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	// Register with retries
	ctx := context.Background()
	chainID := ids.GenerateTestID()
	require.NoError(t, manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
		"/rpc": handler,
	}))
	require.Equal(t, 2, server.failCount) // Failed twice, succeeded on third try
	require.Len(t, server.routes, 2)      // Both alias and ID routes
}

func TestHandlerManager_HealthCheck(t *testing.T) {
	server := newMockServer()
	logger := log.NewNoOpLogger()
	manager := NewHandlerManager(server, logger)

	// Register a healthy handler
	healthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"jsonrpc":"2.0","result":"test","id":1}`))
	})

	// Register an unhealthy handler
	unhealthyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	})

	ctx := context.Background()
	chainID1 := ids.GenerateTestID()
	chainID2 := ids.GenerateTestID()

	// Register healthy chain
	require.NoError(t, manager.RegisterChainHandlers(ctx, chainID1, "", map[string]http.Handler{
		"/rpc": healthyHandler,
	}))

	// Register unhealthy chain
	// Registration succeeds even if health check fails
	require.NoError(t, manager.RegisterChainHandlers(ctx, chainID2, "", map[string]http.Handler{
		"/rpc": unhealthyHandler,
	})) // Registration should succeed regardless of handler health

	// Check health status
	results := manager.HealthCheckAll()
	require.True(t, results[chainID1.String()])
	require.False(t, results[chainID2.String()])
}

func TestHandlerManager_GetBasePaths(t *testing.T) {
	manager := &HandlerManager{}
	chainID := ids.GenerateTestID()

	// Test with alias
	bases := manager.getBasePaths(chainID, "C")
	require.Equal(t, []string{"bc/C", "bc/" + chainID.String()}, bases)

	// Test without alias
	bases = manager.getBasePaths(chainID, "")
	require.Equal(t, []string{"bc/" + chainID.String()}, bases)

	// Test when alias equals chain ID (shouldn't duplicate)
	bases = manager.getBasePaths(chainID, chainID.String())
	require.Equal(t, []string{"bc/" + chainID.String()}, bases)
}

func TestHandlerManager_ContextCancellation(t *testing.T) {
	// Create a server that delays to test cancellation
	server := &mockServer{
		routes:      make(map[string]http.Handler),
		returnError: errors.New("slow server"),
	}

	logger := log.NewNoOpLogger()
	manager := NewHandlerManager(server, logger)
	manager.SetRetryConfig(10, 100*time.Millisecond) // Many retries with delays

	// Create cancelled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	// Try to register - should fail with context error
	chainID := ids.GenerateTestID()
	err := manager.RegisterChainHandlers(ctx, chainID, "", map[string]http.Handler{
		"/rpc": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "context canceled")
}

// Benchmark to ensure performance doesn't degrade
func BenchmarkHandlerRegistration(b *testing.B) {
	server := newMockServer()
	logger := log.NewNoOpLogger()
	manager := NewHandlerManager(server, logger)

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	ctx := context.Background()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		chainID := ids.GenerateTestID()
		manager.RegisterChainHandlers(ctx, chainID, "TEST", map[string]http.Handler{
			"/rpc": handler,
			"/ws":  handler,
		})
	}
}
