Your IP : 216.73.216.224


Current Path : /home/hotlineuser/mobius/hotline/
Upload File :
Current File : //home/hotlineuser/mobius/hotline/server_test.go

package hotline

import (
	"bytes"
	"context"
	"encoding/binary"
	"fmt"
	"io"
	"log/slog"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/charmap"
)

type mockReadWriter struct {
	RBuf bytes.Buffer
	WBuf *bytes.Buffer
}

func (mrw mockReadWriter) Read(p []byte) (n int, err error) {
	return mrw.RBuf.Read(p)
}

func (mrw mockReadWriter) Write(p []byte) (n int, err error) {
	return mrw.WBuf.Write(p)
}

func TestServer_handleFileTransfer(t *testing.T) {
	type fields struct {
		ThreadedNews    *ThreadedNews
		FileTransferMgr FileTransferMgr
		Config          Config
		ConfigDir       string
		Stats           *Stats
		Logger          *slog.Logger
		FS              FileStore
	}
	type args struct {
		ctx context.Context
		rwc io.ReadWriter
	}
	tests := []struct {
		name     string
		fields   fields
		args     args
		wantErr  assert.ErrorAssertionFunc
		wantDump string
	}{
		{
			name: "with invalid protocol",
			args: args{
				ctx: func() context.Context {
					ctx := context.Background()
					ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
					return ctx
				}(),
				rwc: func() io.ReadWriter {
					mrw := mockReadWriter{}
					mrw.WBuf = &bytes.Buffer{}
					mrw.RBuf.Write(
						[]byte{
							0, 0, 0, 0,
							0, 0, 0, 5,
							0, 0, 0x01, 0,
							0, 0, 0, 0,
						},
					)
					return mrw
				}(),
			},
			wantErr: assert.Error,
		},
		{
			name: "with invalid transfer Type",
			fields: fields{
				FileTransferMgr: NewMemFileTransferMgr(),
			},
			args: args{
				ctx: func() context.Context {
					ctx := context.Background()
					ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
					return ctx
				}(),
				rwc: func() io.ReadWriter {
					mrw := mockReadWriter{}
					mrw.WBuf = &bytes.Buffer{}
					mrw.RBuf.Write(
						[]byte{
							0x48, 0x54, 0x58, 0x46,
							0, 0, 0, 5,
							0, 0, 0x01, 0,
							0, 0, 0, 0,
						},
					)
					return mrw
				}(),
			},
			wantErr: assert.Error,
		},
		{
			name: "file download",
			fields: fields{
				FS:     &OSFileStore{},
				Logger: NewTestLogger(),
				Stats:  NewStats(),
				FileTransferMgr: &MemFileTransferMgr{
					fileTransfers: map[FileTransferID]*FileTransfer{
						{0, 0, 0, 5}: {
							RefNum:   [4]byte{0, 0, 0, 5},
							Type:     FileDownload,
							FileName: []byte("testfile-8b"),
							FilePath: []byte{},
							FileRoot: func() string {
								path, _ := os.Getwd()
								return path + "/test/config/Files"
							}(),
							ClientConn: &ClientConn{
								Account: &Account{
									Login: "foo",
								},
								ClientFileTransferMgr: ClientFileTransferMgr{
									transfers: map[FileTransferType]map[FileTransferID]*FileTransfer{
										FileDownload: {
											[4]byte{0, 0, 0, 5}: &FileTransfer{},
										},
									},
								},
							},
							bytesSentCounter: &WriteCounter{},
						},
					},
				},
			},
			args: args{
				ctx: func() context.Context {
					ctx := context.Background()
					ctx = context.WithValue(ctx, contextKeyReq, requestCtx{})
					return ctx
				}(),
				rwc: func() io.ReadWriter {
					mrw := mockReadWriter{}
					mrw.WBuf = &bytes.Buffer{}
					mrw.RBuf.Write(
						[]byte{
							0x48, 0x54, 0x58, 0x46,
							0, 0, 0, 5,
							0, 0, 0x01, 0,
							0, 0, 0, 0,
						},
					)
					return mrw
				}(),
			},
			wantErr: assert.NoError,
			wantDump: `00000000  46 49 4c 50 00 01 00 00  00 00 00 00 00 00 00 00  |FILP............|
00000010  00 00 00 00 00 00 00 02  49 4e 46 4f 00 00 00 00  |........INFO....|
00000020  00 00 00 00 00 00 00 55  41 4d 41 43 54 45 58 54  |.......UAMACTEXT|
00000030  54 54 58 54 00 00 00 00  00 00 01 00 00 00 00 00  |TTXT............|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 0b  |................|
00000070  74 65 73 74 66 69 6c 65  2d 38 62 00 00 44 41 54  |testfile-8b..DAT|
00000080  41 00 00 00 00 00 00 00  00 00 00 00 08 7c 39 e0  |A............|9.|
00000090  bc 64 e2 cd de 4d 41 43  52 00 00 00 00 00 00 00  |.d...MACR.......|
000000a0  00 00 00 00 00                                    |.....|
`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Server{
				FileTransferMgr: tt.fields.FileTransferMgr,
				Config:          tt.fields.Config,
				Logger:          tt.fields.Logger,
				Stats:           tt.fields.Stats,
				FS:              tt.fields.FS,
				TextDecoder:     charmap.Macintosh.NewDecoder(),
				TextEncoder:     charmap.Macintosh.NewEncoder(),
			}

			tt.wantErr(t, s.handleFileTransfer(tt.args.ctx, tt.args.rwc), fmt.Sprintf("handleFileTransfer(%v, %v)", tt.args.ctx, tt.args.rwc))

			assertTransferBytesEqual(t, tt.wantDump, tt.args.rwc.(mockReadWriter).WBuf.Bytes())
		})
	}
}

func TestNewServer_Encoding(t *testing.T) {
	tests := []struct {
		name        string
		encoding    string
		wantDecoder *encoding.Decoder
		wantEncoder *encoding.Encoder
	}{
		{
			name:        "default empty string uses Mac Roman",
			encoding:    "",
			wantDecoder: charmap.Macintosh.NewDecoder(),
			wantEncoder: charmap.Macintosh.NewEncoder(),
		},
		{
			name:        "macintosh uses Mac Roman",
			encoding:    "macintosh",
			wantDecoder: charmap.Macintosh.NewDecoder(),
			wantEncoder: charmap.Macintosh.NewEncoder(),
		},
		{
			name:        "utf8 uses Nop (pass-through)",
			encoding:    "utf8",
			wantDecoder: encoding.Nop.NewDecoder(),
			wantEncoder: encoding.Nop.NewEncoder(),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			srv, err := NewServer(WithConfig(Config{Encoding: tt.encoding}))
			assert.NoError(t, err)

			// Verify encoder/decoder behavior by round-tripping a Mac Roman byte.
			// 0x8e is Mac Roman for "é".
			input := string([]byte{0x8e})

			gotDec, err := srv.TextDecoder.String(input)
			assert.NoError(t, err)
			wantDec, err := tt.wantDecoder.String(input)
			assert.NoError(t, err)
			assert.Equal(t, wantDec, gotDec)

			gotEnc, err := srv.TextEncoder.String("é")
			assert.NoError(t, err)
			wantEnc, err := tt.wantEncoder.String("é")
			assert.NoError(t, err)
			assert.Equal(t, wantEnc, gotEnc)
		})
	}
}

func TestParseTrackerPassword(t *testing.T) {
	tests := []struct {
		name         string
		trackerAddr  string
		wantPassword string
	}{
		{
			name:         "tracker address with password",
			trackerAddr:  "tracker.example.com:5500:mypassword",
			wantPassword: "mypassword",
		},
		{
			name:         "tracker address without password",
			trackerAddr:  "tracker.example.com:5500",
			wantPassword: "",
		},
		{
			name:         "tracker address with empty password",
			trackerAddr:  "tracker.example.com:5500:",
			wantPassword: "",
		},
		{
			name:         "tracker address with password containing special characters",
			trackerAddr:  "tracker.example.com:5500:pass@word#123",
			wantPassword: "pass@word#123",
		},
		{
			name:         "tracker address with password containing colons",
			trackerAddr:  "tracker.example.com:5500:pass:word:123",
			wantPassword: "pass:word:123",
		},
		{
			name:         "IPv4 address with password",
			trackerAddr:  "192.168.1.100:5500:secret",
			wantPassword: "secret",
		},
		{
			name:         "IPv4 address without password",
			trackerAddr:  "192.168.1.100:5500",
			wantPassword: "",
		},
		{
			name:         "malformed address - no port",
			trackerAddr:  "tracker.example.com",
			wantPassword: "",
		},
		{
			name:         "malformed address - empty string",
			trackerAddr:  "",
			wantPassword: "",
		},
		{
			name:         "malformed address - only colons",
			trackerAddr:  ":::",
			wantPassword: ":",
		},
		{
			name:         "IPv6 address handling (edge case - not properly supported)",
			trackerAddr:  "[::1]:5500:password",
			wantPassword: "1]:5500:password", // IPv6 addresses aren't properly handled by simple colon splitting
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := parseTrackerPassword(tt.trackerAddr)
			assert.Equal(t, tt.wantPassword, got)
		})
	}
}

// MockTrackerRegistrar is a mock implementation of TrackerRegistrar for testing
type MockTrackerRegistrar struct {
	RegisterCalls []RegisterCall
	RegisterFunc  func(tracker string, registration *TrackerRegistration) error
}

type RegisterCall struct {
	Tracker      string
	Registration *TrackerRegistration
}

func (m *MockTrackerRegistrar) Register(tracker string, registration *TrackerRegistration) error {
	// Record the call
	m.RegisterCalls = append(m.RegisterCalls, RegisterCall{
		Tracker:      tracker,
		Registration: registration,
	})

	// Use custom function if provided, otherwise return nil (success)
	if m.RegisterFunc != nil {
		return m.RegisterFunc(tracker, registration)
	}
	return nil
}

func (m *MockTrackerRegistrar) Reset() {
	m.RegisterCalls = nil
	m.RegisterFunc = nil
}

func TestServer_registerWithTrackers(t *testing.T) {
	tests := []struct {
		name                      string
		config                    Config
		wantImmediateRegistration bool
		wantTrackerCalls          []string
		mockRegisterFunc          func(tracker string, registration *TrackerRegistration) error
		expectError               bool
	}{
		{
			name: "disabled tracker registration",
			config: Config{
				EnableTrackerRegistration: false,
				Trackers:                  []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
			},
			wantImmediateRegistration: false,
			wantTrackerCalls:          []string{},
		},
		{
			name: "enabled tracker registration with multiple trackers",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			wantImmediateRegistration: true,
			wantTrackerCalls:          []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
		},
		{
			name: "enabled tracker registration with empty tracker list",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			wantImmediateRegistration: true,
			wantTrackerCalls:          []string{},
		},
		{
			name: "tracker registration with network errors",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker1.example.com:5500"},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			wantImmediateRegistration: true,
			wantTrackerCalls:          []string{"tracker1.example.com:5500"},
			mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error {
				return assert.AnError // Simulate network error
			},
			expectError: false, // Errors are logged but don't stop the function
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock registrar
			mockRegistrar := &MockTrackerRegistrar{
				RegisterFunc: tt.mockRegisterFunc,
			}

			// Create server with mock registrar
			server, err := NewServer(
				WithConfig(tt.config),
				WithLogger(NewTestLogger()),
				WithTrackerRegistrar(mockRegistrar),
			)
			assert.NoError(t, err)

			// Create a context that we can cancel
			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			// Start the registerWithTrackers function in a goroutine
			done := make(chan struct{})
			go func() {
				defer close(done)
				server.registerWithTrackers(ctx)
			}()

			// Give it a moment to do the immediate registration
			time.Sleep(100 * time.Millisecond)

			// Cancel the context to stop the goroutine
			cancel()

			// Wait for the goroutine to finish (should be quick after cancellation)
			select {
			case <-done:
				// Success
			case <-time.After(1 * time.Second):
				t.Fatal("registerWithTrackers did not exit after context cancellation")
			}

			// Verify the calls made to the mock registrar
			assert.Len(t, mockRegistrar.RegisterCalls, len(tt.wantTrackerCalls))

			for i, expectedTracker := range tt.wantTrackerCalls {
				if i < len(mockRegistrar.RegisterCalls) {
					call := mockRegistrar.RegisterCalls[i]
					assert.Equal(t, expectedTracker, call.Tracker)
					assert.Equal(t, tt.config.Name, call.Registration.Name)
					assert.Equal(t, tt.config.Description, call.Registration.Description)
					assert.Equal(t, parseTrackerPassword(expectedTracker), call.Registration.Password)
				}
			}
		})
	}
}

func TestServer_registerWithTrackers_ContextCancellation(t *testing.T) {
	tests := []struct {
		name          string
		cancelAfter   time.Duration
		expectedCalls int // Number of expected registration calls before cancellation
		trackerCount  int
	}{
		{
			name:          "immediate cancellation",
			cancelAfter:   10 * time.Millisecond,
			expectedCalls: 2, // Should complete immediate registration
			trackerCount:  2,
		},
		{
			name:          "cancellation after first ticker",
			cancelAfter:   100 * time.Millisecond,
			expectedCalls: 2, // Should only do immediate registration within 100ms
			trackerCount:  2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockRegistrar := &MockTrackerRegistrar{}
			config := Config{
				EnableTrackerRegistration: true,
				Trackers:                  make([]string, tt.trackerCount),
				Name:                      "Test Server",
				Description:               "Test Description",
			}

			// Fill trackers array
			for i := 0; i < tt.trackerCount; i++ {
				config.Trackers[i] = fmt.Sprintf("tracker%d.example.com:5500", i+1)
			}

			server, err := NewServer(
				WithConfig(config),
				WithLogger(NewTestLogger()),
				WithTrackerRegistrar(mockRegistrar),
			)
			assert.NoError(t, err)

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

			done := make(chan struct{})
			go func() {
				defer close(done)
				server.registerWithTrackers(ctx)
			}()

			// Wait for the specified time then cancel
			time.Sleep(tt.cancelAfter)
			cancel()

			// Wait for graceful shutdown
			select {
			case <-done:
				// Success
			case <-time.After(1 * time.Second):
				t.Fatal("registerWithTrackers did not exit after context cancellation")
			}

			// Verify that the function respects context cancellation
			assert.Equal(t, tt.expectedCalls, len(mockRegistrar.RegisterCalls))
		})
	}
}

func TestServer_registerWithTrackers_PeriodicRegistration(t *testing.T) {
	t.Skip("Skipping timing-sensitive test - would take 5+ minutes to run reliably")

	// This test would verify that periodic re-registration happens every trackerUpdateFrequency seconds
	// but it's impractical to run in normal test suites due to the 300-second interval

	mockRegistrar := &MockTrackerRegistrar{}
	config := Config{
		EnableTrackerRegistration: true,
		Trackers:                  []string{"tracker1.example.com:5500"},
		Name:                      "Test Server",
		Description:               "Test Description",
	}

	server, err := NewServer(
		WithConfig(config),
		WithLogger(NewTestLogger()),
		WithTrackerRegistrar(mockRegistrar),
	)
	assert.NoError(t, err)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	done := make(chan struct{})
	go func() {
		defer close(done)
		server.registerWithTrackers(ctx)
	}()

	// Wait for timeout or completion
	<-ctx.Done()

	// Should have done immediate registration only (1 call) in 10 seconds
	// since trackerUpdateFrequency is 300 seconds
	assert.Equal(t, 1, len(mockRegistrar.RegisterCalls))
}

func TestServer_registerWithTrackers_ErrorHandling(t *testing.T) {
	tests := []struct {
		name             string
		trackers         []string
		mockRegisterFunc func(tracker string, registration *TrackerRegistration) error
		expectPanic      bool
	}{
		{
			name:     "handles network errors gracefully",
			trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
			mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error {
				if tracker == "tracker1.example.com:5500" {
					return fmt.Errorf("network error: connection refused")
				}
				return nil // Second tracker succeeds
			},
			expectPanic: false,
		},
		{
			name:     "handles all trackers failing",
			trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500"},
			mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error {
				return fmt.Errorf("network error")
			},
			expectPanic: false,
		},
		{
			name:     "handles empty tracker addresses",
			trackers: []string{"", "valid.tracker.com:5500", ""},
			mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error {
				if tracker == "" {
					return fmt.Errorf("invalid tracker address")
				}
				return nil
			},
			expectPanic: false,
		},
		{
			name:     "handles malformed tracker addresses",
			trackers: []string{"invalid-address", "another:invalid", "valid.tracker.com:5500:password"},
			mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error {
				return nil // Accept all for this test
			},
			expectPanic: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockRegistrar := &MockTrackerRegistrar{
				RegisterFunc: tt.mockRegisterFunc,
			}

			config := Config{
				EnableTrackerRegistration: true,
				Trackers:                  tt.trackers,
				Name:                      "Test Server",
				Description:               "Test Description",
			}

			server, err := NewServer(
				WithConfig(config),
				WithLogger(NewTestLogger()),
				WithTrackerRegistrar(mockRegistrar),
			)
			assert.NoError(t, err)

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

			if tt.expectPanic {
				assert.Panics(t, func() {
					server.registerWithTrackers(ctx)
				})
				return
			}

			done := make(chan struct{})
			go func() {
				defer close(done)
				server.registerWithTrackers(ctx)
			}()

			// Give it time to process
			time.Sleep(50 * time.Millisecond)
			cancel()

			select {
			case <-done:
				// Success - function completed without panicking
			case <-time.After(1 * time.Second):
				t.Fatal("registerWithTrackers did not exit after context cancellation")
			}

			// Verify all trackers were attempted
			assert.Equal(t, len(tt.trackers), len(mockRegistrar.RegisterCalls))
		})
	}
}

func TestServer_registerWithTrackers_EdgeCases(t *testing.T) {
	tests := []struct {
		name           string
		config         Config
		expectedCalls  int
		validateResult func(t *testing.T, calls []RegisterCall)
	}{
		{
			name: "server with zero port",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker.example.com:5500"},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			expectedCalls: 1,
			validateResult: func(t *testing.T, calls []RegisterCall) {
				assert.Equal(t, uint16(0), binary.BigEndian.Uint16(calls[0].Registration.Port[:]))
			},
		},
		{
			name: "server with very long name and description",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker.example.com:5500"},
				Name:                      strings.Repeat("A", 255), // Max uint8 length
				Description:               strings.Repeat("B", 255),
			},
			expectedCalls: 1,
			validateResult: func(t *testing.T, calls []RegisterCall) {
				assert.Equal(t, strings.Repeat("A", 255), calls[0].Registration.Name)
				assert.Equal(t, strings.Repeat("B", 255), calls[0].Registration.Description)
			},
		},
		{
			name: "empty server name and description",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker.example.com:5500"},
				Name:                      "",
				Description:               "",
			},
			expectedCalls: 1,
			validateResult: func(t *testing.T, calls []RegisterCall) {
				assert.Equal(t, "", calls[0].Registration.Name)
				assert.Equal(t, "", calls[0].Registration.Description)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockRegistrar := &MockTrackerRegistrar{}

			server, err := NewServer(
				WithConfig(tt.config),
				WithLogger(NewTestLogger()),
				WithTrackerRegistrar(mockRegistrar),
			)
			assert.NoError(t, err)

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

			done := make(chan struct{})
			go func() {
				defer close(done)
				server.registerWithTrackers(ctx)
			}()

			time.Sleep(50 * time.Millisecond)
			cancel()

			select {
			case <-done:
				// Success
			case <-time.After(1 * time.Second):
				t.Fatal("registerWithTrackers did not exit after context cancellation")
			}

			assert.Equal(t, tt.expectedCalls, len(mockRegistrar.RegisterCalls))
			if tt.validateResult != nil {
				tt.validateResult(t, mockRegistrar.RegisterCalls)
			}
		})
	}
}

func TestServer_CurrentStats(t *testing.T) {
	stats := NewStats()
	stats.Increment(StatCurrentlyConnected)
	stats.Increment(StatDownloadCounter)
	stats.Increment(StatDownloadCounter)

	srv := &Server{Stats: stats}
	result := srv.CurrentStats()

	assert.Equal(t, 1, result["CurrentlyConnected"])
	assert.Equal(t, 2, result["DownloadCounter"])
	assert.Equal(t, 0, result["UploadsInProgress"])
}

func TestServer_sendTransaction(t *testing.T) {
	t.Run("sends transaction to client connection", func(t *testing.T) {
		wBuf := &bytes.Buffer{}
		mockMgr := &MockClientMgr{}
		mockMgr.On("Get", ClientID{0, 1}).Return(&ClientConn{
			Connection: &nopCloserRWC{Buffer: wBuf},
		})

		srv := &Server{ClientMgr: mockMgr}

		tran := NewTransaction(TranChatMsg, ClientID{0, 1}, NewField(FieldData, []byte("hello")))
		err := srv.sendTransaction(tran)

		assert.NoError(t, err)
		assert.Greater(t, wBuf.Len(), 0)
		mockMgr.AssertExpectations(t)
	})

	t.Run("returns nil when client not found", func(t *testing.T) {
		mockMgr := &MockClientMgr{}
		mockMgr.On("Get", ClientID{0, 99}).Return((*ClientConn)(nil))

		srv := &Server{ClientMgr: mockMgr}

		tran := NewTransaction(TranChatMsg, ClientID{0, 99})
		err := srv.sendTransaction(tran)

		assert.NoError(t, err)
		mockMgr.AssertExpectations(t)
	})
}

func TestServer_SendAll(t *testing.T) {
	mockMgr := &MockClientMgr{}
	mockMgr.On("List").Return([]*ClientConn{
		{ID: ClientID{0, 1}},
		{ID: ClientID{0, 2}},
		{ID: ClientID{0, 3}},
	})

	outbox := make(chan Transaction, 10)
	srv := &Server{
		ClientMgr: mockMgr,
		outbox:    outbox,
	}

	srv.SendAll(TranChatMsg, NewField(FieldData, []byte("broadcast")))

	assert.Len(t, outbox, 3)

	// Verify each transaction targets a different client
	clientIDs := make(map[ClientID]bool)
	for range 3 {
		tran := <-outbox
		clientIDs[tran.ClientID] = true
		assert.Equal(t, TranChatMsg, tran.Type)
	}
	assert.True(t, clientIDs[ClientID{0, 1}])
	assert.True(t, clientIDs[ClientID{0, 2}])
	assert.True(t, clientIDs[ClientID{0, 3}])

	mockMgr.AssertExpectations(t)
}

type nopCloserRWC struct {
	*bytes.Buffer
}

func (n *nopCloserRWC) Close() error { return nil }

func TestServer_NewClientConn(t *testing.T) {
	mockMgr := &MockClientMgr{}
	mockMgr.On("Add", mock.AnythingOfType("*hotline.ClientConn")).Return()

	srv := &Server{ClientMgr: mockMgr}

	rwc := &nopCloserRWC{Buffer: &bytes.Buffer{}}

	cc := srv.NewClientConn(rwc, "192.168.1.1:12345")

	assert.NotNil(t, cc)
	assert.Equal(t, "192.168.1.1:12345", cc.RemoteAddr)
	assert.Equal(t, []byte{0, 0}, cc.Icon)
	assert.Equal(t, srv, cc.Server)
	mockMgr.AssertExpectations(t)
}

func TestServer_registerWithAllTrackers(t *testing.T) {
	tests := []struct {
		name                      string
		config                    Config
		expectRegistrationAttempt bool
		expectedTrackerCalls      []string
	}{
		{
			name: "disabled tracker registration",
			config: Config{
				EnableTrackerRegistration: false,
				Trackers:                  []string{"tracker1.example.com:5500"},
			},
			expectRegistrationAttempt: false,
			expectedTrackerCalls:      []string{},
		},
		{
			name: "enabled tracker registration with multiple trackers",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			expectRegistrationAttempt: true,
			expectedTrackerCalls:      []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"},
		},
		{
			name: "enabled tracker registration with empty tracker list",
			config: Config{
				EnableTrackerRegistration: true,
				Trackers:                  []string{},
				Name:                      "Test Server",
				Description:               "Test Description",
			},
			expectRegistrationAttempt: true,
			expectedTrackerCalls:      []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockRegistrar := &MockTrackerRegistrar{}

			server, err := NewServer(
				WithConfig(tt.config),
				WithLogger(NewTestLogger()),
				WithTrackerRegistrar(mockRegistrar),
			)
			assert.NoError(t, err)

			// Call the extracted function directly
			server.registerWithAllTrackers()

			// Verify the expected number of calls
			assert.Equal(t, len(tt.expectedTrackerCalls), len(mockRegistrar.RegisterCalls))

			// Verify each call
			for i, expectedTracker := range tt.expectedTrackerCalls {
				if i < len(mockRegistrar.RegisterCalls) {
					call := mockRegistrar.RegisterCalls[i]
					assert.Equal(t, expectedTracker, call.Tracker)
					assert.Equal(t, tt.config.Name, call.Registration.Name)
					assert.Equal(t, tt.config.Description, call.Registration.Description)
					assert.Equal(t, parseTrackerPassword(expectedTracker), call.Registration.Password)
				}
			}
		})
	}
}

func TestSendBanMessage(t *testing.T) {
	buf := &bytes.Buffer{}
	sendBanMessage(buf, "You are banned")

	assert.Greater(t, buf.Len(), 0)
	assert.Contains(t, buf.String(), "You are banned")
}