package public

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/versioning"
)

type fakeMessageSearchStore struct{ supported bool }

func (store fakeMessageSearchStore) SupportsMessageSearch(context.Context) bool {
	return store.supported
}

type fakeAdminDB struct{}

func (fakeAdminDB) Ping(context.Context) error { return nil }
func (fakeAdminDB) Close()                     {}

type fakeMigrationInspector struct {
	state  coredb.MigrationState
	err    error
	called bool
}

func (inspector *fakeMigrationInspector) InspectMigrationState(context.Context, coredb.AdminDB) (coredb.MigrationState, error) {
	inspector.called = true
	return inspector.state, inspector.err
}

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

	router := gin.New()
	handler := &PublicConfigHandler{
		store:                      fakeMessageSearchStore{supported: true},
		pingCAPAuthEnabled:         true,
		passwordAuthDisabled:       true,
		openRegistration:           false,
		registrationMode:           "invite_only",
		teamsEnabled:               true,
		agentToolsSettingsEnabled:  true,
		billingEnabled:             true,
		daemonConnectBaseURL:       "https://configured.example",
		meshXEnabled:               true,
		meshXKnowledgeBasesEnabled: true,
		meshXRolesEnabled:          true,
		meshXBaseURL:               " https://meshx.example ",
	}
	router.GET("/api/v1/public/config", handler.Get)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	var payload dto.AppPublicConfigResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.True(t, payload.PingCAPAuthEnabled)
	require.False(t, payload.PasswordAuthEnabled)
	require.False(t, payload.OpenRegistration)
	require.Equal(t, "invite_only", payload.RegistrationMode)
	require.Zero(t, payload.MessageHistoryDays)
	require.True(t, payload.MessageSearchEnabled)
	require.True(t, payload.TeamsEnabled)
	require.True(t, payload.AgentToolsSettingsEnabled)
	require.True(t, payload.BillingEnabled)
	require.Equal(t, "https://configured.example", payload.DaemonConnectBaseURL)
	require.True(t, payload.MeshXEnabled)
	require.True(t, payload.MeshXKnowledgeBasesEnabled)
	require.True(t, payload.MeshXRolesEnabled)
	require.Equal(t, "https://meshx.example", payload.MeshXBaseURL)
	require.True(t, payload.DesktopQuickStartEnabled)
	require.NotEmpty(t, payload.ServerVersion)
	require.NotEmpty(t, payload.ServerGitHash)
}

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

	for _, test := range []struct {
		name string
		raw  string
		want string
	}{
		{name: "empty defaults open", raw: "", want: registrationModeOpen},
		{name: "open lowercases and trims", raw: " OPEN ", want: registrationModeOpen},
		{name: "invite only lowercases and trims", raw: " Invite_Only ", want: registrationModeInviteOnly},
		{name: "unknown is normalized but preserved", raw: " Custom ", want: "custom"},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, test.want, normalizeRegistrationMode(test.raw))
		})
	}
}

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

	request := httptest.NewRequest(http.MethodGet, "http://internal.example/api/v1/public/config", nil)
	request.Header.Set("X-Forwarded-Proto", "http")
	request.Header.Set("X-Forwarded-Host", "wrong.example")

	require.Equal(t, "https://agentteam.pingcap.com/", daemonConnectBaseURLForRequest(request, " https://agentteam.pingcap.com/ "))
}

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

	request := httptest.NewRequest(http.MethodGet, "http://internal.example/api/v1/public/config", nil)
	request.Host = "internal.example"
	request.Header.Set("X-Forwarded-Proto", "https, http")
	request.Header.Set("X-Forwarded-Host", "agentteam.example.com, internal.example")

	require.Equal(t, "https://agentteam.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	request := httptest.NewRequest(http.MethodGet, "https://internal.example/api/v1/public/config", nil)
	request.Host = "agentteam.example.com"
	request.TLS = &tls.ConnectionState{}
	request.Header.Set("X-Forwarded-Proto", "ftp")

	require.Equal(t, "https://agentteam.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	require.Equal(t, "agentteam.example.com", forwardedHeaderValue(" agentteam.example.com, internal.example "))
	require.Empty(t, forwardedHeaderValue(" , internal.example "))
}

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

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Host = "direct.example.com"

	require.Equal(t, "http://direct.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Host = "plain.example.com"

	require.Equal(t, "http://plain.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Host = "secure.example.com"
	request.TLS = &tls.ConnectionState{}

	require.Equal(t, "https://secure.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Host = "mixed.example.com"
	request.Header.Set("X-Forwarded-Proto", "ftp")

	require.Equal(t, "http://mixed.example.com", daemonConnectBaseURLForRequest(request, ""))
}

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

	require.Equal(t, "only-host", forwardedHeaderValue("only-host"))
}

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

	require.Empty(t, forwardedHeaderValue(""))
}

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

	router := gin.New()
	handler := &PublicConfigHandler{}
	router.GET("/api/v1/public/config", handler.Get)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	var payload dto.AppPublicConfigResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.True(t, payload.PasswordAuthEnabled)
	require.False(t, payload.AgentToolsSettingsEnabled)
	require.False(t, payload.BillingEnabled)
	require.False(t, payload.DesktopQuickStartEnabled)
	require.False(t, payload.MeshXKnowledgeBasesEnabled)
	require.False(t, payload.MeshXRolesEnabled)
}

func TestPublicConfigHandlerGetIncludesStagingRuntimeInfo(t *testing.T) {
	previousBuildTime := versioning.BuildTime
	versioning.BuildTime = "2026-05-24T03:04:05Z"
	t.Cleanup(func() { versioning.BuildTime = previousBuildTime })

	router := gin.New()
	inspector := &fakeMigrationInspector{
		state: coredb.MigrationState{CurrentVersion: 147, TargetVersion: 147},
	}
	handler := &PublicConfigHandler{
		environment:        " Staging ",
		migrationInspector: inspector,
		coreDB:             fakeAdminDB{},
	}
	router.GET("/api/v1/public/config", handler.Get)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	var payload dto.AppPublicConfigResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.True(t, inspector.called)
	require.Equal(t, "staging", payload.DeploymentEnvironment)
	require.Equal(t, "2026-05-24T03:04:05Z", payload.ServerBuildTime)
	require.NotNil(t, payload.CoreSchemaVersion)
	require.EqualValues(t, 147, *payload.CoreSchemaVersion)
}

func TestPublicConfigHandlerGetHidesRuntimeInfoOutsideStaging(t *testing.T) {
	router := gin.New()
	inspector := &fakeMigrationInspector{
		state: coredb.MigrationState{CurrentVersion: 147, TargetVersion: 147},
	}
	handler := &PublicConfigHandler{
		environment:        "production",
		migrationInspector: inspector,
		coreDB:             fakeAdminDB{},
	}
	router.GET("/api/v1/public/config", handler.Get)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	var payload dto.AppPublicConfigResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.False(t, inspector.called)
	require.Empty(t, payload.DeploymentEnvironment)
	require.Empty(t, payload.ServerBuildTime)
	require.Nil(t, payload.CoreSchemaVersion)
}

func TestPublicConfigHandlerGetKeepsStagingConfigAvailableWhenMigrationStateFails(t *testing.T) {
	router := gin.New()
	inspector := &fakeMigrationInspector{err: errors.New("migration unavailable")}
	handler := &PublicConfigHandler{
		environment:        "staging",
		migrationInspector: inspector,
		coreDB:             fakeAdminDB{},
	}
	router.GET("/api/v1/public/config", handler.Get)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/public/config", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	var payload dto.AppPublicConfigResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.True(t, inspector.called)
	require.Equal(t, "staging", payload.DeploymentEnvironment)
	require.Nil(t, payload.CoreSchemaVersion)
}
