package server

import (
	"context"
	"os/user"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	gstatus "google.golang.org/grpc/status"

	"github.com/netbirdio/netbird/client/internal/profilemanager"
	"github.com/netbirdio/netbird/client/mdm"
	"github.com/netbirdio/netbird/client/proto"
)

// withMDMPolicy temporarily overrides the server-package loadMDMPolicy hook
// so SetConfig observes the supplied Policy. Restores the original loader
// at test cleanup.
func withMDMPolicy(t *testing.T, policy *mdm.Policy) {
	t.Helper()
	prev := loadMDMPolicy
	loadMDMPolicy = func() *mdm.Policy { return policy }
	t.Cleanup(func() { loadMDMPolicy = prev })
}

// setupServerWithProfile mirrors the boilerplate of TestSetConfig_AllFieldsSaved:
// overrides profilemanager paths to a temp dir, seeds a profile, sets it
// active, and constructs a Server instance. Returns the constructed server
// plus context + profile name + username + cfgPath for the seeded profile.
func setupServerWithProfile(t *testing.T) (s *Server, ctx context.Context, profName, username, cfgPath string) {
	t.Helper()
	tempDir := t.TempDir()

	origDefaultProfileDir := profilemanager.DefaultConfigPathDir
	origDefaultConfigPath := profilemanager.DefaultConfigPath
	origActiveProfileStatePath := profilemanager.ActiveProfileStatePath
	profilemanager.ConfigDirOverride = tempDir
	profilemanager.DefaultConfigPathDir = tempDir
	profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
	profilemanager.DefaultConfigPath = filepath.Join(tempDir, "default.json")
	t.Cleanup(func() {
		profilemanager.DefaultConfigPathDir = origDefaultProfileDir
		profilemanager.ActiveProfileStatePath = origActiveProfileStatePath
		profilemanager.DefaultConfigPath = origDefaultConfigPath
		profilemanager.ConfigDirOverride = ""
	})

	currUser, err := user.Current()
	require.NoError(t, err)

	profName = "test-profile-mdm"
	cfgPath = filepath.Join(tempDir, profName+".json")

	_, err = profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
		ConfigPath:    cfgPath,
		ManagementURL: "https://api.netbird.io:443",
	})
	require.NoError(t, err)

	pm := profilemanager.ServiceManager{}
	require.NoError(t, pm.SetActiveProfileState(&profilemanager.ActiveProfileState{
		ID:       profilemanager.ID(profName),
		Username: currUser.Username,
	}))

	ctx = context.Background()
	s = New(ctx, "console", "", false, false, false, false)
	return s, ctx, profName, currUser.Username, cfgPath
}

// extractViolation pulls the MDMManagedFieldsViolation detail from a
// FailedPrecondition error. Fails the test if absent or malformed.
func extractViolation(t *testing.T, err error) *proto.MDMManagedFieldsViolation {
	t.Helper()
	require.Error(t, err)
	st, ok := gstatus.FromError(err)
	require.True(t, ok, "error must be a gRPC status: %v", err)
	require.Equal(t, codes.FailedPrecondition, st.Code(), "expected FailedPrecondition, got %s", st.Code())
	for _, d := range st.Details() {
		if v, ok := d.(*proto.MDMManagedFieldsViolation); ok {
			return v
		}
	}
	t.Fatalf("MDMManagedFieldsViolation detail not found on status; details: %v", st.Details())
	return nil
}

func TestSetConfig_MDMReject_SingleField(t *testing.T) {
	withMDMPolicy(t, mdm.NewPolicy(map[string]any{
		mdm.KeyManagementURL: "https://mdm.example.com:443",
	}))

	s, ctx, profName, username, _ := setupServerWithProfile(t)

	_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
		ProfileName:   profName,
		Username:      username,
		ManagementUrl: "https://user.tried.this.com:443",
	})

	v := extractViolation(t, err)
	assert.Equal(t, []string{mdm.KeyManagementURL}, v.GetFields())
}

func TestSetConfig_MDMReject_MultipleFields(t *testing.T) {
	withMDMPolicy(t, mdm.NewPolicy(map[string]any{
		mdm.KeyManagementURL:    "https://mdm.example.com:443",
		mdm.KeyBlockInbound:     true,
		mdm.KeyRosenpassEnabled: true,
	}))

	s, ctx, profName, username, _ := setupServerWithProfile(t)

	blockInbound := false
	rosenpassEnabled := false
	_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
		ProfileName:      profName,
		Username:         username,
		ManagementUrl:    "https://user.tried.this.com:443",
		BlockInbound:     &blockInbound,
		RosenpassEnabled: &rosenpassEnabled,
	})

	v := extractViolation(t, err)
	assert.ElementsMatch(t, []string{
		mdm.KeyManagementURL,
		mdm.KeyBlockInbound,
		mdm.KeyRosenpassEnabled,
	}, v.GetFields())
}

func TestSetConfig_MDMReject_AllOrNothing(t *testing.T) {
	// MDM enforces ManagementURL only; user request touches both the
	// enforced field AND a non-enforced field (RosenpassEnabled).
	// The whole request must be rejected — non-conflicting fields are not
	// applied either.
	withMDMPolicy(t, mdm.NewPolicy(map[string]any{
		mdm.KeyManagementURL: "https://mdm.example.com:443",
	}))

	s, ctx, profName, username, cfgPath := setupServerWithProfile(t)

	rosenpassEnabled := true
	_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
		ProfileName:      profName,
		Username:         username,
		ManagementUrl:    "https://user.tried.this.com:443",
		RosenpassEnabled: &rosenpassEnabled,
	})

	v := extractViolation(t, err)
	assert.Equal(t, []string{mdm.KeyManagementURL}, v.GetFields())

	// Confirm RosenpassEnabled was NOT applied even though it was not
	// in the conflict list: the request was rejected as a whole.
	reloaded, err := profilemanager.GetConfig(cfgPath)
	require.NoError(t, err)
	assert.False(t, reloaded.RosenpassEnabled, "non-conflicting field must not be applied when request is rejected")
}

func TestSetConfig_MDMAllow_NonManagedFields(t *testing.T) {
	// MDM enforces ManagementURL but the user only writes RosenpassEnabled.
	// Request must succeed.
	withMDMPolicy(t, mdm.NewPolicy(map[string]any{
		mdm.KeyManagementURL: "https://mdm.example.com:443",
	}))

	s, ctx, profName, username, _ := setupServerWithProfile(t)

	rosenpassEnabled := true
	resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
		ProfileName:      profName,
		Username:         username,
		RosenpassEnabled: &rosenpassEnabled,
	})

	require.NoError(t, err)
	require.NotNil(t, resp)
}

// TestSetConfig_MDMAllow_ManagementURLPortNormalized covers the
// regression from discussion #6483: MDM URL without explicit port vs
// UI echo with the parseURL-appended default port must be treated as
// a no-op echo, not a conflict.
func TestSetConfig_MDMAllow_ManagementURLPortNormalized(t *testing.T) {
	tests := []struct {
		name      string
		mdmURL    string
		submitURL string
	}{
		{"policy_no_port_submit_with_443", "https://netbird.corp.example", "https://netbird.corp.example:443"},
		{"policy_with_443_submit_no_port", "https://netbird.corp.example:443", "https://netbird.corp.example"},
		{"http_policy_no_port_submit_with_80", "http://netbird.corp.example", "http://netbird.corp.example:80"},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			withMDMPolicy(t, mdm.NewPolicy(map[string]any{
				mdm.KeyManagementURL: tc.mdmURL,
			}))

			s, ctx, profName, username, _ := setupServerWithProfile(t)

			rosenpassEnabled := true
			resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
				ProfileName:      profName,
				Username:         username,
				ManagementUrl:    tc.submitURL,
				RosenpassEnabled: &rosenpassEnabled,
			})

			require.NoError(t, err, "port-normalized URL echo must not trip MDM conflict gate")
			require.NotNil(t, resp)
		})
	}
}

func TestSetConfig_MDMEmpty_NoEnforcement(t *testing.T) {
	// No MDM policy active: any field can be written.
	withMDMPolicy(t, mdm.NewPolicy(nil))

	s, ctx, profName, username, _ := setupServerWithProfile(t)

	resp, err := s.SetConfig(ctx, &proto.SetConfigRequest{
		ProfileName:   profName,
		Username:      username,
		ManagementUrl: "https://user.changed.url.com:443",
	})

	require.NoError(t, err)
	require.NotNil(t, resp)
}
