// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package newrelic

import (
	"errors"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/newrelic/go-agent/v3/internal"
	"github.com/newrelic/go-agent/v3/internal/logger"
)

// some of these tests were initially generated by Github Copilot and/or TabNine and then tweaked by a human

func TestConnectTraceObserver_RestartCalled(t *testing.T) {
	app := &app{}
	mockObs := &mockTraceObserver{}
	app.setObserver(mockObs)

	reply := &internal.ConnectReply{
		RunID:             internal.AgentRunID("test-run-id"),
		RequestHeadersMap: map[string]string{"header": "value"},
	}

	app.connectTraceObserver(reply)

	if !mockObs.restartCalled {
		t.Error("expected restart to be called on observer")
	}
	if mockObs.lastRunID != reply.RunID {
		t.Errorf("expected RunID %v, got %v", reply.RunID, mockObs.lastRunID)
	}
	if mockObs.lastHeaders["header"] != "value" {
		t.Errorf("expected header value 'value', got %v", mockObs.lastHeaders["header"])
	}
}

func TestConnectBackoff(t *testing.T) {
	attempts := map[int]int{
		0:   15,
		2:   30,
		5:   300,
		6:   300,
		100: 300,
		-5:  300,
	}

	for k, v := range attempts {
		if b := getConnectBackoffTime(k); b != v {
			t.Error(fmt.Sprintf("Invalid connect backoff for attempt #%d:", k), v)
		}
	}
}

func TestProcessConnectMessages(t *testing.T) {
	testCases := []struct {
		name           string
		level          string
		message        string
		expectedMethod string
	}{
		{
			name:           "error level",
			level:          "ERROR",
			message:        "An error occurred",
			expectedMethod: "Error",
		},
		{
			name:           "warn level",
			level:          "WARN",
			message:        "A warning occurred",
			expectedMethod: "Warn",
		},
		{
			name:           "info level",
			level:          "INFO",
			message:        "An info message",
			expectedMethod: "Info",
		},
		{
			name:           "debug level",
			level:          "DEBUG",
			message:        "A debug message",
			expectedMethod: "Debug",
		},
		{
			name:           "verbose level",
			level:          "VERBOSE",
			message:        "A verbose message",
			expectedMethod: "Debug",
		},
		{
			name:           "unknown level",
			level:          "unknown",
			message:        "An unknown level message",
			expectedMethod: "",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tl := &testLoggerWithMethodTracking{}

			run := &appRun{
				Reply: &internal.ConnectReply{
					Messages: []struct {
						Message string `json:"message"`
						Level   string `json:"level"`
					}{
						{
							Message: tc.message,
							Level:   tc.level,
						},
					},
				},
			}

			processConnectMessages(run, tl)

			if tc.expectedMethod == "" {
				// For unknown levels, no method should be called
				if len(tl.calledMethods) != 0 {
					t.Errorf("Expected no logger methods to be called for unknown level, but got: %v", tl.calledMethods)
				}
			} else {
				// Check that the correct method was called
				if len(tl.calledMethods) != 1 {
					t.Errorf("Expected exactly one logger method to be called, but got: %v", tl.calledMethods)
				} else if tl.calledMethods[0] != tc.expectedMethod {
					t.Errorf("Expected %s method to be called, but got: %s", tc.expectedMethod, tl.calledMethods[0])
				}

				// Check that the message was logged correctly
				if len(tl.messages) != 1 {
					t.Errorf("Expected exactly one message to be logged, but got: %v", tl.messages)
				} else if tl.messages[0] != "collector message" {
					t.Errorf("Expected 'collector message' to be logged, but got: %s", tl.messages[0])
				}

				// Check that the fields contain the original message
				if len(tl.fields) != 1 {
					t.Errorf("Expected exactly one field map, but got: %v", tl.fields)
				} else if msg, ok := tl.fields[0]["msg"]; !ok || msg != tc.message {
					t.Errorf("Expected field 'msg' to be '%s', but got: %v", tc.message, msg)
				}
			}
		})
	}
}

func TestProcess(t *testing.T) {
	testApp := newTestApp(nil, func(cfg *Config) {
		cfg.Enabled = true
	})

	app := testApp.Application.app
	app.config.Enabled = true

	// Ensure all channels are initialized
	if app.dataChan == nil {
		app.dataChan = make(chan appData, 1)
	}
	if app.initiateShutdown == nil {
		app.initiateShutdown = make(chan time.Duration, 1)
	}
	if app.collectorErrorChan == nil {
		app.collectorErrorChan = make(chan rpmResponse, 1)
	}
	if app.connectChan == nil {
		app.connectChan = make(chan *appRun, 1)
	}
	if app.shutdownStarted == nil {
		app.shutdownStarted = make(chan struct{})
	}
	if app.shutdownComplete == nil {
		app.shutdownComplete = make(chan struct{})
	}

	run := &appRun{
		Reply: &internal.ConnectReply{
			Messages: []struct {
				Message string `json:"message"`
				Level   string `json:"level"`
			}{
				{
					Message: "test message",
					Level:   "INFO",
				},
			},
			RunID: "test-run-id",
		},
	}
	app.run = run

	harvest := newHarvest(time.Now(), run.harvestConfig)

	processDone := make(chan struct{})
	go func() {
		app.process()
		close(processDone)
	}()

	// Case 1: Harvest ticker triggers
	harvestReady := make(chan struct{})
	go func() {
		harvest.Ready(time.Now())
		close(harvestReady)
	}()
	select {
	case <-harvestReady:
	case <-time.After(500 * time.Millisecond):
		t.Error("harvest did not become ready in time")
	}

	// Case 2: Data channel receives data
	dataSent := make(chan struct{})
	go func() {
		app.dataChan <- appData{
			id:   run.Reply.RunID,
			data: customMetric{RawInputName: "testMetric", Value: 42},
		}
		close(dataSent)
	}()
	select {
	case <-dataSent:
	case <-time.After(500 * time.Millisecond):
		t.Error("data was not sent in time")
	}

	// Case 3: Shutdown initiated
	shutdownInitiated := make(chan struct{})
	go func() {
		app.initiateShutdown <- 1 * time.Second
		close(shutdownInitiated)
	}()
	select {
	case <-shutdownInitiated:
	case <-time.After(500 * time.Millisecond):
		t.Error("shutdown was not initiated in time")
	}

	// Case 4: Collector error channel receives a response
	collectorErrorSent := make(chan struct{})
	go func() {
		app.collectorErrorChan <- rpmResponse{statusCode: 410, err: errors.New("disconnect")}
		close(collectorErrorSent)
	}()
	select {
	case <-collectorErrorSent:
	case <-time.After(500 * time.Millisecond):
		t.Error("collector error was not sent in time")
	}

	// Case 5: Connect channel receives a new run
	connectSent := make(chan struct{})
	go func() {
		app.connectChan <- newAppRun(app.config, run.Reply)
		close(connectSent)
	}()
	select {
	case <-connectSent:
	case <-time.After(500 * time.Millisecond):
		t.Error("connect was not sent in time")
	}

	select {
	case <-processDone:
	case <-time.After(2 * time.Second):
		t.Error("process did not finish in time")
	}

	app.Shutdown(1 * time.Second)
}

func TestProcess_HarvestAndShutdown(t *testing.T) {
	// Setup a test app with enabled config
	app := newTestApp(
		func(reply *internal.ConnectReply) {
			reply.RunID = "test-run-id"
			reply.CollectCustomEvents = true
			reply.SecurityPolicies.CustomEvents.SetEnabled(true)
		},
		func(cfg *Config) {
			cfg.Enabled = true
			cfg.AppName = "test-app"
			cfg.License = testLicenseKey
		},
	)

	// Start the app's process in a goroutine
	go app.Application.app.process()

	// Send a custom event to trigger dataChan case
	app.Application.RecordCustomEvent("TestEvent", map[string]interface{}{"foo": "bar"})

	// Initiate shutdown
	timeout := 100 * time.Millisecond
	app.Application.app.initiateShutdown <- timeout

	// Wait for shutdown to complete
	select {
	case <-app.Application.app.shutdownComplete:
	case <-time.After(2 * timeout):
		t.Fatal("shutdown did not complete in time")
	}
}

func TestAppProcess_CollectorErrorChan_Disconnect(t *testing.T) {
	app := newTestApp(
		func(reply *internal.ConnectReply) {
			reply.RunID = internal.AgentRunID("test-run-id")
		},
		func(cfg *Config) {
			cfg.Enabled = true
			cfg.AppName = "test-app"
			cfg.License = testLicenseKey
		},
	)

	go app.Application.app.process()

	// Send a disconnect error to collectorErrorChan
	resp := rpmResponse{statusCode: 410, err: errors.New("disconnect")}
	app.Application.app.collectorErrorChan <- resp

	// Wait for shutdown to complete
	select {
	case <-app.Application.app.shutdownComplete:
	case <-time.After(200 * time.Millisecond):
		// Not all disconnects will close shutdownComplete, so just check state
		if app.Application.app.run != nil {
			t.Error("app run should be nil after disconnect")
		}
	}
}

func TestAppProcess_CollectorErrorChan_RestartException(t *testing.T) {
	app := newTestApp(
		func(reply *internal.ConnectReply) {
			reply.RunID = internal.AgentRunID("test-run-id")
		},
		func(cfg *Config) {
			cfg.Enabled = true
			cfg.AppName = "test-app"
			cfg.License = testLicenseKey
		},
	)

	go app.Application.app.process()

	// Send a restart exception error (statusCode 401) to collectorErrorChan
	resp := rpmResponse{statusCode: 401, err: errors.New("restart exception")}
	app.Application.app.collectorErrorChan <- resp

	// Wait for shutdown to complete
	select {
	case <-app.Application.app.shutdownComplete:
	case <-time.After(200 * time.Millisecond):
		// Not all restart exceptions will close shutdownComplete, so just check state
		if app.Application.app.run != nil {
			t.Error("app run should be nil after restart exception")
		}
	}
}

func TestAppProcess_ConnectChan_TraceObserverVariants(t *testing.T) {
	type testCase struct {
		name                string
		setRunTraceObserver bool
		checkConfig         func(app expectApplication, run *appRun) bool
		expectShouldUse     bool
	}

	testCases := []testCase{
		{
			name:                "should use trace observer from app config",
			setRunTraceObserver: false,
			checkConfig: func(app expectApplication, run *appRun) bool {
				return shouldUseTraceObserver(app.app.config)
			},
			expectShouldUse: true,
		},
		{
			name:                "should use trace observer from run config",
			setRunTraceObserver: true,
			checkConfig: func(app expectApplication, run *appRun) bool {
				return shouldUseTraceObserver(run.Config)
			},
			expectShouldUse: true,
		},
		{
			name:                "should use trace observer from app config only",
			setRunTraceObserver: false,
			checkConfig: func(app expectApplication, run *appRun) bool {
				return shouldUseTraceObserver(app.app.config)
			},
			expectShouldUse: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			testApp := newTestApp(
				func(reply *internal.ConnectReply) {
					reply.RunID = internal.AgentRunID("test-run-id")
					reply.CollectCustomEvents = true
					reply.SecurityPolicies.CustomEvents.SetEnabled(true)
				},
				func(cfg *Config) {
					cfg.Enabled = true
					cfg.AppName = "test-app"
					cfg.License = testLicenseKey
					cfg.DistributedTracer.Enabled = true
					cfg.SpanEvents.Enabled = true
				},
			)

			testApp.app.config.traceObserverURL = &observerURL{host: "localhost"}

			go testApp.app.process()

			run := testApp.app.placeholderRun
			if tc.setRunTraceObserver {
				run.Config.traceObserverURL = &observerURL{host: "localhost"}
			}
			testApp.app.connectChan <- run

			if got := tc.checkConfig(testApp, run); got != tc.expectShouldUse {
				t.Errorf("shouldUseTraceObserver returned %v, want %v", got, tc.expectShouldUse)
			}

			timeout := 100 * time.Millisecond
			testApp.app.initiateShutdown <- timeout

			select {
			case <-testApp.app.shutdownComplete:
			case <-time.After(2 * timeout):
				t.Fatal("shutdown did not complete in time")
			}
		})
	}
}

func TestShutdownCoreLogic(t *testing.T) {
	t.Run("shutdown with nil app", func(t *testing.T) {
		app := &Application{}
		start := time.Now()
		app.app.Shutdown(2 * time.Second)
		elapsed := time.Since(start)
		if elapsed > 10*time.Millisecond {
			t.Errorf("Shutdown took too long for nil app: %v", elapsed)
		}
	})

	t.Run("shutdown with config disabled", func(t *testing.T) {
		testApp := newTestApp(nil, func(cfg *Config) {
			cfg.Enabled = false
		})

		start := time.Now()
		testApp.app.Shutdown(2 * time.Second)
		elapsed := time.Since(start)
		if elapsed > 10*time.Millisecond {
			t.Errorf("Shutdown took too long for disabled config: %v", elapsed)
		}
	})

	t.Run("shutdown with serverless", func(t *testing.T) {
		testApp := newTestApp(nil, func(cfg *Config) {
			cfg.Enabled = true
			cfg.ServerlessMode.Enabled = true
		})
		start := time.Now()
		testApp.app.Shutdown(2 * time.Second)
		elapsed := time.Since(start)
		if elapsed > 10*time.Millisecond {
			t.Errorf("Shutdown took too long for serverless app: %v", elapsed)
		}
	})

	t.Run("shutdown with running app", func(t *testing.T) {
		tl := &testLogger{}
		testApp := newTestApp(nil, func(cfg *Config) {
			cfg.Enabled = true
			cfg.Logger = tl
		})
		testApp.app.config.Enabled = true
		time.Sleep(100 * time.Millisecond)
		start := time.Now()
		testApp.app.Shutdown(50 * time.Millisecond)
		elapsed := time.Since(start)
		if elapsed < 50*time.Millisecond || elapsed > 150*time.Millisecond {
			t.Errorf("Expected timeout around 50ms, got %v", elapsed)
		}
		found := false
		for _, msg := range tl.messages {
			if msg == "application shutdown" {
				found = true
				break
			}
		}
		if !found {
			t.Error("Expected 'application shutdown' log message not found")
		}
	})

	t.Run("shutdown timeout case - timer expires", func(t *testing.T) {
		testApp := newTestApp(nil, func(cfg *Config) {
			cfg.Enabled = true
		})
		time.Sleep(100 * time.Millisecond)
		testApp.app.config.Enabled = true
		start := time.Now()
		testApp.app.Shutdown(1 * time.Millisecond)
		elapsed := time.Since(start)
		if elapsed < 1*time.Millisecond || elapsed > 50*time.Millisecond {
			t.Errorf("Expected timeout around 1ms, got %v", elapsed)
		}
	})

	t.Run("multiple shutdown calls - default case coverage", func(t *testing.T) {
		testApp := newTestApp(nil, func(cfg *Config) {
			cfg.Enabled = true
		})
		time.Sleep(100 * time.Millisecond)
		done1 := make(chan bool)
		go func() {
			testApp.app.config.Enabled = true
			testApp.app.Shutdown(2 * time.Second)
			done1 <- true
		}()
		time.Sleep(10 * time.Millisecond)
		done2 := make(chan bool)
		go func() {
			testApp.app.config.Enabled = true
			testApp.app.Shutdown(1 * time.Second)
			done2 <- true
		}()
		select {
		case <-done1:
		case <-time.After(3 * time.Second):
			t.Error("First shutdown timed out")
		}
		select {
		case <-done2:
		case <-time.After(2 * time.Second):
			t.Error("Second shutdown timed out")
		}
	})
}

func TestRunSampler(t *testing.T) {
	t.Run("run sampler with shutdown", func(t *testing.T) {
		testApp := newTestApp(sampleEverythingReplyFn, configTestAppLogFn)
		app := testApp.app
		app.config.RuntimeSampler.Enabled = true

		// Create a channel to track when sampler exits
		samplerDone := make(chan struct{})

		// Start the sampler in a goroutine
		go func() {
			defer close(samplerDone)
			runSampler(app, 50*time.Millisecond) // Short period for testing
		}()

		// Let it run for a bit
		time.Sleep(100 * time.Millisecond)

		// Trigger shutdown
		close(app.shutdownStarted)

		// Wait for sampler to exit
		select {
		case <-samplerDone:
			// Success - sampler exited
		case <-time.After(time.Second):
			t.Error("sampler did not exit after shutdown signal")
		}
	})

	t.Run("run sampler collects data", func(t *testing.T) {
		testApp := newTestApp(sampleEverythingReplyFn, configTestAppSamplerFn)
		app := testApp.app

		// Initialize test harvest so Consume works properly
		app.HarvestTesting(nil)

		// Ensure the app has a valid run state with RunID
		run, _ := app.getState()
		if run.Reply.RunID == "" {
			// Set a test run ID so Consume works
			reply := *run.Reply
			reply.RunID = "test-run-id"
			testRun := &appRun{
				Reply:         &reply,
				Config:        run.Config,
				harvestConfig: run.harvestConfig,
			}
			app.setState(testRun, nil)
		}

		// Start the sampler
		go func() {
			runSampler(app, 50*time.Millisecond)
		}()

		// Wait for data to be collected by checking if any metrics exist
		ticker := time.NewTicker(10 * time.Millisecond)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				// Check for any system metrics by trying to validate an empty list
				// If metrics exist, ExpectMetricsPresent won't call Error
				app.ExpectMetricsPresent(testApp, []internal.WantMetric{})

				app.ExpectTxnMetrics(testApp, internal.WantTxn{})

				// If we get here without errors, metrics were found
				close(app.shutdownStarted)
				return
			case <-time.After(250 * time.Millisecond):
				t.Error("no system stats data received")
				close(app.shutdownStarted)
				return
			}
		}
	})
}

func TestWaitForConnection(t *testing.T) {
	t.Run("early returns", func(t *testing.T) {
		testCases := []struct {
			name   string
			app    *app
			config func(*Config)
		}{
			{"nil app", nil, nil},
			{"disabled app", nil, func(cfg *Config) { cfg.Enabled = false }},
			{"serverless app", nil, func(cfg *Config) { cfg.ServerlessMode.Enabled = true }},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				var app *app
				if tc.config != nil {
					testApp := newTestApp(nil, tc.config)

					app = testApp.app
				}

				err := app.WaitForConnection(1 * time.Second)
				if err != nil {
					t.Errorf("Expected nil error, got: %v", err)
				}
				app.Shutdown(1 * time.Second)
			})
		}
	})

	t.Run("timeout cases", func(t *testing.T) {
		testCases := []struct {
			name    string
			setupFn func(*app)
		}{
			{
				"no connection",
				func(app *app) { app.config.Enabled = true },
			},
			{
				"empty run id",
				func(app *app) {
					app.config.Enabled = true
					run := app.placeholderRun
					reply := *run.Reply
					reply.RunID = ""
					app.setState(&appRun{Reply: &reply, Config: run.Config, harvestConfig: run.harvestConfig}, nil)
				},
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true })

				tc.setupFn(testApp.app)

				start := time.Now()
				err := testApp.app.WaitForConnection(100 * time.Millisecond)
				elapsed := time.Since(start)

				if err == nil || !strings.Contains(err.Error(), "timeout") {
					t.Errorf("Expected timeout error, got: %v", err)
				}
				if elapsed < 90*time.Millisecond || elapsed > 200*time.Millisecond {
					t.Errorf("Expected elapsed time around 100ms, got: %v", elapsed)
				}
				testApp.Shutdown(10 * time.Second)
			})
		}
	})

	t.Run("successful connections", func(t *testing.T) {
		testCases := []struct {
			name              string
			replyFn           func(*internal.ConnectReply)
			configFn          func(*Config)
			setupObserver     bool
			observerConnected bool
		}{
			{
				"without trace observer",
				func(reply *internal.ConnectReply) { reply.RunID = "test-run-123" },
				func(cfg *Config) { cfg.Enabled = true },
				false, false,
			},
			{
				"with connected trace observer",
				func(reply *internal.ConnectReply) { reply.RunID = "test-run-456" },
				func(cfg *Config) {
					cfg.Enabled = true
					cfg.DistributedTracer.Enabled = true
					cfg.SpanEvents.Enabled = true
				},
				true, true,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				testApp := newTestApp(tc.replyFn, tc.configFn)

				testApp.app.config.Enabled = true

				if tc.setupObserver {
					testApp.app.setObserver(&mockTraceObserver{connected: tc.observerConnected})
				}

				// Setup connected state
				run := testApp.app.placeholderRun
				reply := *run.Reply
				config := run.Config
				if tc.setupObserver {
					config.DistributedTracer.Enabled = true
					config.SpanEvents.Enabled = true
				}

				testApp.app.setState(&appRun{
					Reply: &reply, Config: config, harvestConfig: run.harvestConfig,
				}, nil)

				err := testApp.app.WaitForConnection(1 * time.Second)
				if err != nil {
					t.Errorf("Expected nil error, got: %v", err)
				}
				testApp.Shutdown(1 * time.Second)
			})
		}
	})

	t.Run("app error state", func(t *testing.T) {
		testApp := newTestApp(nil, func(cfg *Config) { cfg.Enabled = true })

		testApp.app.config.Enabled = true

		expectedErr := errors.New("connection failed")
		testApp.app.setState(nil, expectedErr)

		err := testApp.app.WaitForConnection(1 * time.Second)
		if !errors.Is(err, expectedErr) {
			t.Errorf("Expected error %v, got: %v", expectedErr, err)
		}
		testApp.Shutdown(1 * time.Second)
	})
}

func TestNewApp(t *testing.T) {
	testCases := []struct {
		name           string
		configFn       func(*Config)
		expectEnabled  bool
		expectChannels bool
		expectLogger   bool
	}{
		{
			"disabled app",
			func(cfg *Config) {
				cfg.Enabled = false
				cfg.AppName = "test-app"
				cfg.License = testLicenseKey
			},
			false, true, true,
		},
		{
			"enabled app",
			func(cfg *Config) {
				cfg.Enabled = true
				cfg.AppName = "test-app"
				cfg.License = testLicenseKey
			},
			true, true, true,
		},
		{
			"serverless app",
			func(cfg *Config) {
				cfg.Enabled = true
				cfg.ServerlessMode.Enabled = true
				cfg.AppName = "test-app"
				cfg.License = testLicenseKey
			},
			true, true, true,
		},
		{
			"runtime sampler enabled",
			func(cfg *Config) {
				cfg.Enabled = true
				cfg.RuntimeSampler.Enabled = true
				cfg.AppName = "test-app"
				cfg.License = testLicenseKey
			},
			true, true, true,
		},
		{
			"custom transport",
			func(cfg *Config) {
				cfg.Transport = &http.Transport{}
				cfg.AppName = "test-app"
				cfg.License = testLicenseKey
			},
			false, true, true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := &Config{
				AppName: "",
				License: "",
				Logger:  new(testLogger),
			}
			tc.configFn(cfg)

			app := newApp(config{Config: *cfg})

			if app == nil {
				t.Fatal("Expected non-nil app")
			}

			if app.config.Enabled != tc.expectEnabled {
				t.Errorf("Expected enabled=%v, got %v", tc.expectEnabled, app.config.Enabled)
			}

			if tc.expectChannels {
				if app.initiateShutdown == nil {
					t.Error("Expected initiateShutdown channel to be initialized")
				}
				if app.shutdownStarted == nil {
					t.Error("Expected shutdownStarted channel to be initialized")
				}
				if app.shutdownComplete == nil {
					t.Error("Expected shutdownComplete channel to be initialized")
				}
				if app.connectChan == nil {
					t.Error("Expected connectChan to be initialized")
				}
				if app.collectorErrorChan == nil {
					t.Error("Expected collectorErrorChan to be initialized")
				}
				if app.dataChan == nil {
					t.Error("Expected dataChan to be initialized")
				}
			}

			if tc.expectLogger && app.Logger == nil {
				t.Error("Expected logger to be set")
			}

			if app.placeholderRun == nil {
				t.Error("Expected placeholderRun to be initialized")
			}

			if app.rpmControls.Client == nil {
				t.Error("Expected HTTP client to be initialized")
			}
			if app.rpmControls.GzipWriterPool == nil {
				t.Error("Expected gzip writer pool to be initialized")
			}

			if cfg.ServerlessMode.Enabled && app.config.Enabled {
				if app.serverless == nil {
					t.Error("Expected serverless harvest to be initialized")
				}
				if app.run == nil {
					t.Error("Expected run to be set for serverless mode")
				}
			}

			if tc.configFn != nil && app.config.Transport == nil {
				if app.rpmControls.Client.Transport != collectorDefaultTransport {
					t.Error("Expected default transport to be used when none specified")
				}
			}
			app.Shutdown(1 * time.Second)
		})
	}
}

func TestNewAppChannelBuffering(t *testing.T) {
	cfg := &Config{
		AppName: "test-app",
		License: testLicenseKey,
		Enabled: false,
		Logger:  new(testLogger),
	}

	app := newApp(config{Config: *cfg})

	// Test that initiateShutdown is buffered (size 1)
	select {
	case app.initiateShutdown <- 1 * time.Second:
		// Should succeed without blocking
	default:
		t.Error("initiateShutdown channel should be buffered")
	}

	// Second send should not block but also shouldn't succeed due to buffer full
	select {
	case app.initiateShutdown <- 1 * time.Second:
		t.Error("Second send should not succeed on buffered channel of size 1")
	default:
		// Expected behavior
	}

	// Test connectChan buffering
	run := &appRun{}
	select {
	case app.connectChan <- run:
		// Should succeed without blocking
	default:
		t.Error("connectChan should be buffered")
	}

	// Test collectorErrorChan buffering
	resp := rpmResponse{}
	select {
	case app.collectorErrorChan <- resp:
		// Should succeed without blocking
	default:
		t.Error("collectorErrorChan should be buffered")
	}
}

func TestShouldUseTraceObserver(t *testing.T) {
	testCases := []struct {
		name                string
		traceObserverURLSet bool
		spanEventsEnabled   bool
		distributedEnabled  bool
		expect              bool
	}{
		{
			name:                "all required fields enabled",
			traceObserverURLSet: true,
			spanEventsEnabled:   true,
			distributedEnabled:  true,
			expect:              true,
		},
		{
			name:                "missing traceObserverURL",
			traceObserverURLSet: false,
			spanEventsEnabled:   true,
			distributedEnabled:  true,
			expect:              false,
		},
		{
			name:                "spanEvents disabled",
			traceObserverURLSet: true,
			spanEventsEnabled:   false,
			distributedEnabled:  true,
			expect:              false,
		},
		{
			name:                "distributed tracer disabled",
			traceObserverURLSet: true,
			spanEventsEnabled:   true,
			distributedEnabled:  false,
			expect:              false,
		},
		{
			name:                "all disabled",
			traceObserverURLSet: false,
			spanEventsEnabled:   false,
			distributedEnabled:  false,
			expect:              false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var url *observerURL
			if tc.traceObserverURLSet {
				u := observerURL{host: "localhost"}
				url = &u
			}
			c := config{}
			c.traceObserverURL = url
			c.SpanEvents.Enabled = tc.spanEventsEnabled
			c.DistributedTracer.Enabled = tc.distributedEnabled

			got := shouldUseTraceObserver(c)
			if got != tc.expect {
				t.Errorf("shouldUseTraceObserver(%+v) = %v, want %v", c, got, tc.expect)
			}
		})
	}
}

func TestHarvestTesting(t *testing.T) {
	testCases := []struct {
		name       string
		replyFn    func(*internal.ConnectReply)
		expectGUID string
	}{
		{
			name: "replyfn sets EntityGUID",
			replyFn: func(reply *internal.ConnectReply) {
				reply.EntityGUID = "customGUID"
			},
			expectGUID: "customGUID",
		},
		{
			name:       "replyfn is nil, default EntityGUID",
			replyFn:    nil,
			expectGUID: "",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			app := newApp(config{Config: Config{
				AppName: "test-app",
				License: testLicenseKey,
				Logger:  new(testLogger),
			}})

			app.HarvestTesting(tc.replyFn)

			app.Shutdown(10 * time.Second)

			if app.testHarvest == nil {
				t.Fatal("expected testHarvest to be initialized")
			}
			if app.placeholderRun == nil {
				t.Fatal("expected placeholderRun to be initialized")
			}
			gotGUID := app.placeholderRun.Reply.EntityGUID
			if gotGUID != tc.expectGUID {
				t.Errorf("expected EntityGUID %q, got %q", tc.expectGUID, gotGUID)
			}
		})
	}
}

func TestNilAppReturnsNil(t *testing.T) {
	var app *app
	txn := app.StartTransaction("test-txn")
	if txn != nil {
		t.Errorf("Expected StartTransaction on nil app to return nil, got: %#v", txn)
	}

	err := app.RecordCustomEvent("TestEvent", map[string]interface{}{})

	if err != nil {
		t.Errorf("Expected RecordCustomEvent on nil app to return nil, got: %#v", txn)
	}

	err = app.RecordCustomMetric("TestMetric", 1.0)

	if err != nil {
		t.Errorf("Expected RecordCustomMetric on nil app to return nil, got: %#v", txn)
	}
}

func TestRecordCustomEventUnlimitedSizeTypes(t *testing.T) {
	eventTypes := []string{
		"LlmEmbedding",
		"LlmChatCompletionSummary",
		"LlmChatCompletionMessage",
	}
	params := map[string]interface{}{"foo": "bar"}
	for _, et := range eventTypes {
		t.Run(et, func(t *testing.T) {
			app := newTestApp(nil)

			err := app.app.RecordCustomEvent(et, params)
			if err != nil {
				t.Errorf("expected nil error for eventType %s, got %v", et, err)
			}

			app.Shutdown(10 * time.Second)

			// Validate that the event was recorded with the correct type and params
			app.ExpectCustomEvents(t, []internal.WantEvent{{
				Intrinsics: map[string]interface{}{
					"type":      et,
					"timestamp": internal.MatchAnything,
				},
				UserAttributes: params,
			}})
		})
	}
}

func TestRecordLog(t *testing.T) {
	testCases := []struct {
		name    string
		logData LogData
		wantLog internal.WantLog
	}{
		{
			name: "basic log event",
			logData: LogData{
				Severity:  "Debug",
				Message:   "Test Message",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "Debug",
				Message:  "Test Message",
			},
		},
		{
			name: "info log event",
			logData: LogData{
				Severity:  "Info",
				Message:   "Info Message",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "Info",
				Message:  "Info Message",
			},
		},
		{
			name: "warn log event",
			logData: LogData{
				Severity:  "Warn",
				Message:   "Warn Message",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "Warn",
				Message:  "Warn Message",
			},
		},
		{
			name: "error log event",
			logData: LogData{
				Severity:  "Error",
				Message:   "Error Message",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "Error",
				Message:  "Error Message",
			},
		},
		{
			name: "empty message log event",
			logData: LogData{
				Severity:  "Info",
				Message:   "",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "Info",
				Message:  "",
			},
		},
		{
			name: "empty severity log event",
			logData: LogData{
				Severity:  "",
				Message:   "No Severity",
				Timestamp: int64(timeToUnixMilliseconds(time.Now())),
			},
			wantLog: internal.WantLog{
				Severity: "UNKNOWN",
				Message:  "No Severity",
			},
		},
		{
			name: "future timestamp log event",
			logData: LogData{
				Severity:  "Info",
				Message:   "Future Timestamp",
				Timestamp: int64(timeToUnixMilliseconds(time.Now().Add(24 * time.Hour))),
			},
			wantLog: internal.WantLog{
				Severity: "Info",
				Message:  "Future Timestamp",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			testApp := newTestApp(
				sampleEverythingReplyFn,
				configTestAppLogFn,
			)

			// Ensure timestamps match for comparison
			if tc.logData.Timestamp != 0 {
				tc.logData.Timestamp = int64(timeToUnixMilliseconds(time.Now()))
				tc.wantLog.Timestamp = tc.logData.Timestamp
			}

			testApp.RecordLog(tc.logData)

			testApp.ExpectLogEvents(t, []internal.WantLog{
				tc.wantLog,
			})
			testApp.Shutdown(1 * time.Second)
		})
	}

	t.Run("ApplicationLogging disabled returns nil", func(t *testing.T) {
		testApp := newTestApp(
			sampleEverythingReplyFn,
			func(cfg *Config) {
				cfg.Enabled = true
				cfg.ApplicationLogging.Enabled = false
			},
		)
		defer testApp.Shutdown(10 * time.Second)

		logData := LogData{
			Severity:  "Info",
			Message:   "Should not record",
			Timestamp: int64(timeToUnixMilliseconds(time.Now())),
		}

		err := testApp.app.RecordLog(&logData)
		if err != nil {
			return
		}
		if err != nil {
			t.Errorf("Expected nil error when ApplicationLogging is disabled, got: %v", err)
		}
		testApp.ExpectLogEvents(t, []internal.WantLog{})
	})
}

func TestConsumeIDBehavior(t *testing.T) {
	testCases := []struct {
		name       string
		id         string
		expectData bool
	}{
		{
			name:       "empty id does not send data",
			id:         "",
			expectData: false,
		},
		{
			name:       "non-empty id sends data",
			id:         "1234",
			expectData: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			app := newApp(config{Config: Config{
				AppName: "test-app",
				License: testLicenseKey,
				Logger:  new(testLogger),
			}})

			app.serverless = newServerlessHarvest(logger.ShimLogger{}, serverlessGetenvShim)

			// Ensure dataChan is initialized
			if app.dataChan == nil {
				app.dataChan = make(chan appData, 1)
			}

			app.Consume(internal.AgentRunID(tc.id), customMetric{RawInputName: "shouldNotSend", Value: 123})

			select {
			case <-app.dataChan:
				if !tc.expectData {
					t.Error("expected no data to be sent to dataChan when id is empty")
				}
			default:
				if tc.expectData {
					t.Error("expected data to be sent to dataChan when id is not empty")
				}
			}
			app.Shutdown(100 * time.Millisecond)
		})
	}
}

func TestTransactionExpectLogEventsMergesThreadData(t *testing.T) {
	testApp := newTestApp(sampleEverythingReplyFn, configTestAppLogFn)

	app := testApp.app

	// Create a transaction and manually add a log event to its thread's harvest
	txn := app.StartTransaction("txn-log-test")
	logEvent := &logEvent{
		severity:  "Info",
		message:   "Thread log message",
		timestamp: int64(timeToUnixMilliseconds(time.Now())),
	}
	// Simulate log event in thread's logs
	txn.thread.StoreLog(logEvent)

	// Ensure app.testHarvest is empty before ExpectLogEvents
	if len(app.testHarvest.LogEvents.logs) != 0 {
		t.Fatalf("Expected app.testHarvest.LogEvents to be empty before merge")
	}

	want := []internal.WantLog{
		{
			Severity:  "Info",
			Message:   "Thread log message",
			Timestamp: logEvent.timestamp,
		},
	}

	txn.ExpectLogEvents(t, want)

	// After ExpectLogEvents, app.testHarvest should contain the merged log event
	if len(app.testHarvest.LogEvents.logs) == 0 {
		t.Errorf("Expected app.testHarvest.LogEvents to contain merged thread log event")
	}

	testApp.Shutdown(1 * time.Second)
}

type mockTraceObserver struct {
	connected     bool
	restartCalled bool
	lastRunID     internal.AgentRunID
	lastHeaders   map[string]string
}

func (m *mockTraceObserver) initialConnCompleted() bool { return m.connected }
func (m *mockTraceObserver) restart(runID internal.AgentRunID, headers map[string]string) {
	m.restartCalled = true
	m.lastRunID = runID
	m.lastHeaders = headers
}
func (m *mockTraceObserver) shutdown(time.Duration) error                  { return nil }
func (m *mockTraceObserver) QueueSize() int                                { return 0 }
func (m *mockTraceObserver) Consume(*spanEvent)                            {}
func (m *mockTraceObserver) consumeSpan(*spanEvent)                        {}
func (m *mockTraceObserver) dumpSupportabilityMetrics() map[string]float64 { return nil }

const (
	sampleAppName = "my app"
)

// expectApp combines Application and Expect, for use in validating data in test apps
type expectApplication struct {
	internal.Expect
	*Application
}

func (e expectApplication) Error(i ...interface{}) {
}

func newTestApp(replyfn func(*internal.ConnectReply), cfgFn ...ConfigOption) expectApplication {
	cfgFn = append(cfgFn,
		func(cfg *Config) {
			// Prevent spawning app goroutines in tests.
			if !cfg.ServerlessMode.Enabled {
				cfg.Enabled = false
			}
		},
		ConfigAppName(sampleAppName),
		ConfigLicense(testLicenseKey),
		ConfigCodeLevelMetricsEnabled(false),
	)

	app, err := NewApplication(cfgFn...)
	if nil != err {
		panic(err)
	}

	internal.HarvestTesting(app.Private, replyfn)

	return expectApplication{
		Expect:      app.Private.(internal.Expect),
		Application: app,
	}
}

const (
	testEntityGUID = "testEntityGUID123"
)

var sampleEverythingReplyFn = func(reply *internal.ConnectReply) {
	reply.SetSampleEverything()
	reply.EntityGUID = testEntityGUID
}

var configTestAppLogFn = func(cfg *Config) {
	cfg.Enabled = false
	cfg.ApplicationLogging.Enabled = true
	cfg.ApplicationLogging.Forwarding.Enabled = true
	cfg.ApplicationLogging.Metrics.Enabled = true
}

var configTestAppSamplerFn = func(cfg *Config) {
	cfg.Enabled = true
	cfg.RuntimeSampler.Enabled = true
	cfg.ServerlessMode.Enabled = true
}

type testLoggerWithMethodTracking struct {
	messages      []string
	fields        []map[string]interface{}
	calledMethods []string
}

func (tl *testLoggerWithMethodTracking) DebugEnabled() bool {
	return true
}

func (tl *testLoggerWithMethodTracking) Error(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
	tl.fields = append(tl.fields, fields)
	tl.calledMethods = append(tl.calledMethods, "Error")
}

func (tl *testLoggerWithMethodTracking) Warn(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
	tl.fields = append(tl.fields, fields)
	tl.calledMethods = append(tl.calledMethods, "Warn")
}

func (tl *testLoggerWithMethodTracking) Info(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
	tl.fields = append(tl.fields, fields)
	tl.calledMethods = append(tl.calledMethods, "Info")
}

func (tl *testLoggerWithMethodTracking) Debug(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
	tl.fields = append(tl.fields, fields)
	tl.calledMethods = append(tl.calledMethods, "Debug")
}

type testLogger struct {
	messages []string
}

func (tl *testLogger) DebugEnabled() bool {
	return true
}

func (tl *testLogger) Error(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
}
func (tl *testLogger) Warn(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
}
func (tl *testLogger) Info(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
}
func (tl *testLogger) Debug(msg string, fields map[string]interface{}) {
	tl.messages = append(tl.messages, msg)
}
