package gateway

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"git.dcentral.systems/toolz/goplt/internal/client"
	"git.dcentral.systems/toolz/goplt/pkg/config"
	"git.dcentral.systems/toolz/goplt/pkg/logger"
	"git.dcentral.systems/toolz/goplt/pkg/registry"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestMain sets up the test environment before running tests.
func TestMain(m *testing.M) {
	// Set Gin to test mode once for all tests to avoid race conditions
	gin.SetMode(gin.TestMode)
	// Run tests
	os.Exit(m.Run())
}

func TestNewGateway(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		cfg     config.ConfigProvider
		log     logger.Logger
		factory *client.ServiceClientFactory
		reg     registry.ServiceRegistry
		wantErr bool
	}{
		{
			name:    "successful creation",
			cfg:     &mockConfigProvider{},
			log:     &mockLogger{},
			factory: &client.ServiceClientFactory{},
			reg:     &mockServiceRegistry{},
			wantErr: false,
		},
		{
			name:    "nil config",
			cfg:     nil,
			log:     &mockLogger{},
			factory: &client.ServiceClientFactory{},
			reg:     &mockServiceRegistry{},
			wantErr: false, // NewGateway doesn't validate nil config currently
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gateway, err := NewGateway(tt.cfg, tt.log, tt.factory, tt.reg)
			if tt.wantErr {
				require.Error(t, err)
				assert.Nil(t, gateway)
			} else {
				require.NoError(t, err)
				require.NotNil(t, gateway)
				assert.Equal(t, tt.cfg, gateway.config)
				assert.Equal(t, tt.log, gateway.log)
				assert.Equal(t, tt.factory, gateway.clientFactory)
				assert.Equal(t, tt.reg, gateway.registry)
			}
		})
	}
}

func TestGateway_SetupRoutes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		routes         []RouteConfig
		requestPath    string
		expectedStatus int
	}{
		{
			name: "route configured",
			routes: []RouteConfig{
				{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
			},
			requestPath:    "/api/v1/test",
			expectedStatus: http.StatusServiceUnavailable, // Will fail service discovery
		},
		{
			name:           "no routes configured",
			routes:         []RouteConfig{},
			requestPath:    "/api/v1/test",
			expectedStatus: http.StatusNotFound, // NoRoute handler
		},
		{
			name: "unmatched route",
			routes: []RouteConfig{
				{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
			},
			requestPath:    "/api/v1/other",
			expectedStatus: http.StatusNotFound, // NoRoute handler
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gateway := &Gateway{
				config:        &mockConfigProvider{},
				log:           &mockLogger{},
				clientFactory: &client.ServiceClientFactory{},
				registry:      &mockServiceRegistry{},
				routes:        tt.routes,
			}

			router := gin.New()
			gateway.SetupRoutes(router)

			req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tt.expectedStatus, w.Code)
		})
	}
}

func TestGateway_handleRoute(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		route          RouteConfig
		registrySetup  func() *mockServiceRegistry
		requestPath    string
		expectedStatus int
	}{
		{
			name: "service discovery failure",
			route: RouteConfig{
				Path:    "/api/v1/test",
				Service: "test-service",
			},
			registrySetup: func() *mockServiceRegistry {
				return &mockServiceRegistry{
					discoverError: errors.New("service discovery failed"),
				}
			},
			requestPath:    "/api/v1/test",
			expectedStatus: http.StatusServiceUnavailable,
		},
		{
			name: "no service instances",
			route: RouteConfig{
				Path:    "/api/v1/test",
				Service: "test-service",
			},
			registrySetup: func() *mockServiceRegistry {
				return &mockServiceRegistry{
					instances: []*registry.ServiceInstance{},
				}
			},
			requestPath:    "/api/v1/test",
			expectedStatus: http.StatusServiceUnavailable,
		},
		// Note: Testing invalid target URL and proxy forwarding requires integration tests
		// with real HTTP servers, as httptest.ResponseRecorder doesn't support CloseNotify
		// which is required by the reverse proxy.
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockReg := tt.registrySetup()
			gateway := &Gateway{
				config:        &mockConfigProvider{},
				log:           &mockLogger{},
				clientFactory: &client.ServiceClientFactory{},
				registry:      mockReg,
				routes:        []RouteConfig{tt.route},
			}

			router := gin.New()
			gateway.SetupRoutes(router)

			req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tt.expectedStatus, w.Code)
		})
	}
}

// Note: Proxy forwarding tests should be in integration tests with testcontainers.
// The reverse proxy requires a real HTTP connection which httptest.ResponseRecorder
// cannot provide due to missing CloseNotify interface.

func TestGateway_handleRoute_AllMethods(t *testing.T) {
	t.Parallel()

	methods := []string{
		http.MethodGet,
		http.MethodPost,
		http.MethodPut,
		http.MethodDelete,
		http.MethodPatch,
		http.MethodOptions,
		http.MethodHead,
	}

	// Use a registry that will fail discovery to avoid proxy forwarding issues
	mockReg := &mockServiceRegistry{
		instances: []*registry.ServiceInstance{}, // Empty to trigger service unavailable
	}

	gateway := &Gateway{
		config:        &mockConfigProvider{},
		log:           &mockLogger{},
		clientFactory: &client.ServiceClientFactory{},
		registry:      mockReg,
		routes: []RouteConfig{
			{Path: "/api/v1/test", Service: "test-service"},
		},
	}

	router := gin.New()
	gateway.SetupRoutes(router)

	for _, method := range methods {
		t.Run(method, func(t *testing.T) {
			req := httptest.NewRequest(method, "/api/v1/test", nil)
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			// All methods should be handled (route should match, even if service unavailable)
			assert.NotEqual(t, http.StatusNotFound, w.Code, "Route should match for %s", method)
		})
	}
}

func TestLoadRoutes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		cfg  config.ConfigProvider
	}{
		{
			name: "empty config",
			cfg:  &mockConfigProvider{},
		},
		{
			name: "nil config",
			cfg:  nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			routes := loadRoutes(tt.cfg)
			// Currently loadRoutes returns empty slice
			// This test ensures it doesn't panic
			assert.NotNil(t, routes)
		})
	}
}

func TestGateway_NoRoute(t *testing.T) {
	t.Parallel()

	gateway := &Gateway{
		config:        &mockConfigProvider{},
		log:           &mockLogger{},
		clientFactory: &client.ServiceClientFactory{},
		registry:      &mockServiceRegistry{},
		routes:        []RouteConfig{},
	}

	router := gin.New()
	gateway.SetupRoutes(router)

	req := httptest.NewRequest(http.MethodGet, "/unknown/path", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusNotFound, w.Code)

	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	require.NoError(t, err)
	assert.Equal(t, "Route not found", response["error"])
	assert.Equal(t, "/unknown/path", response["path"])
}

// mockConfigProvider implements config.ConfigProvider for testing.
type mockConfigProvider struct {
	values map[string]any
}

func (m *mockConfigProvider) Get(key string) any {
	if m.values == nil {
		return nil
	}
	return m.values[key]
}

func (m *mockConfigProvider) GetString(key string) string {
	if m.values == nil {
		return ""
	}
	if val, ok := m.values[key].(string); ok {
		return val
	}
	return ""
}

func (m *mockConfigProvider) GetInt(key string) int {
	if m.values == nil {
		return 0
	}
	if val, ok := m.values[key].(int); ok {
		return val
	}
	return 0
}

func (m *mockConfigProvider) GetBool(key string) bool {
	if m.values == nil {
		return false
	}
	if val, ok := m.values[key].(bool); ok {
		return val
	}
	return false
}

func (m *mockConfigProvider) GetDuration(key string) time.Duration {
	return 0
}

func (m *mockConfigProvider) GetStringSlice(key string) []string {
	if m.values == nil {
		return nil
	}
	if val, ok := m.values[key].([]string); ok {
		return val
	}
	return nil
}

func (m *mockConfigProvider) IsSet(key string) bool {
	if m.values == nil {
		return false
	}
	_, ok := m.values[key]
	return ok
}

func (m *mockConfigProvider) Unmarshal(v any) error {
	return nil
}

// mockLogger implements logger.Logger for testing.
type mockLogger struct {
	infoLogs  []string
	errorLogs []string
	warnLogs  []string
}

func (m *mockLogger) Debug(msg string, fields ...logger.Field) {}
func (m *mockLogger) Info(msg string, fields ...logger.Field) {
	m.infoLogs = append(m.infoLogs, msg)
}
func (m *mockLogger) Warn(msg string, fields ...logger.Field) {
	m.warnLogs = append(m.warnLogs, msg)
}
func (m *mockLogger) Error(msg string, fields ...logger.Field) {
	m.errorLogs = append(m.errorLogs, msg)
}
func (m *mockLogger) With(fields ...logger.Field) logger.Logger {
	return m
}
func (m *mockLogger) WithContext(ctx context.Context) logger.Logger {
	return m
}

// mockServiceRegistry implements registry.ServiceRegistry for testing.
type mockServiceRegistry struct {
	instances     []*registry.ServiceInstance
	discoverError error
	registerError error
	deregError    error
}

func (m *mockServiceRegistry) Register(ctx context.Context, service *registry.ServiceInstance) error {
	return m.registerError
}

func (m *mockServiceRegistry) Deregister(ctx context.Context, serviceID string) error {
	return m.deregError
}

func (m *mockServiceRegistry) Discover(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
	if m.discoverError != nil {
		return nil, m.discoverError
	}
	return m.instances, nil
}

func (m *mockServiceRegistry) Watch(ctx context.Context, serviceName string) (<-chan []*registry.ServiceInstance, error) {
	ch := make(chan []*registry.ServiceInstance, 1)
	ch <- m.instances
	close(ch)
	return ch, nil
}

func (m *mockServiceRegistry) Health(ctx context.Context, serviceID string) (*registry.HealthStatus, error) {
	return &registry.HealthStatus{
		ServiceID: serviceID,
		Status:    "healthy",
	}, nil
}
