//go:build !windows && integration
// +build !windows,integration

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package stats

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"
	"time"

	"github.com/aws/amazon-ecs-agent/agent/api/serviceconnect"

	apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi"
	ecsengine "github.com/aws/amazon-ecs-agent/agent/engine"
	"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
	apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status"
	"github.com/docker/docker/api/types"
	dockercontainer "github.com/docker/docker/api/types/container"
	"github.com/gorilla/mux"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testStatsRestPath = "/get/stats"
	testStatsRestURL  = "http://testhost" + testStatsRestPath
)

const (
	testStats = `# TYPE MetricFamily3 histogram
		MetricFamily3{dimensionX="value1", dimensionY="value2", le="0.5"} 1
		MetricFamily3{dimensionX="value1", dimensionY="value2", le="1"} 2
		MetricFamily3{dimensionX="value1", dimensionY="value2", le="5"} 3
		`
)

func TestStatsEngineWithNetworkStatsDifferentModes(t *testing.T) {
	var networkModes = []struct {
		NetworkMode string
		StatsEmpty  bool
	}{
		{"bridge", false},
		{"host", true},
		{"none", true},
	}
	for _, tc := range networkModes {
		testNetworkModeStatsInteg(t, tc.NetworkMode, tc.StatsEmpty)
	}
}

func TestStatsEngineWithServiceConnectMetrics(t *testing.T) {
	testcases := []struct {
		name                 string
		shouldDisableMetrics bool
	}{
		{
			name: "Test Stats engine for Service Connect task with metrics enabled",
		},
		{
			name:                 "Test Stats engine for Service Connect task with metrics disabled",
			shouldDisableMetrics: true,
		},
	}
	testUDSPath := filepath.Join(t.TempDir(), "test_stats_metrics.sock")
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			testConfig := cfg
			if tc.shouldDisableMetrics {
				testConfig.DisableMetrics = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}
			}

			// Create a new docker stats engine
			engine := NewDockerStatsEngine(&testConfig, dockerClient, eventStream("TestStatsEngineWithServiceConnectMetrics"), nil, nil, nil)
			ctx, cancel := context.WithCancel(context.TODO())
			defer cancel()

			// Assign ContainerStop timeout to addressable variable
			timeout := int(defaultDockerTimeoutSeconds)
			containerOptions := dockercontainer.StopOptions{
				Timeout: &timeout,
			}

			// Create a container to get the container id.
			container, err := createGremlin(client, "default")
			require.NoError(t, err, "creating container failed")
			defer client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true})

			engine.cluster = defaultCluster
			engine.containerInstanceArn = defaultContainerInstance

			err = client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{})
			require.NoError(t, err, "starting container failed")
			defer client.ContainerStop(ctx, container.ID, containerOptions)

			containerChangeEventStream := eventStream("TestStatsEngineWithServiceConnectMetrics")
			taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream,
				nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil, nil)
			testTask := createRunningTask("bridge")
			testTask.ServiceConnectConfig = &serviceconnect.Config{
				ContainerName: serviceConnectContainerName,
				RuntimeConfig: serviceconnect.RuntimeConfig{
					AdminSocketPath: testUDSPath,
					StatsRequest:    testStatsRestURL,
				},
			}
			// Populate Tasks and Container map in the engine.
			dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine)
			dockerTaskEngine.State().AddTask(testTask)
			dockerTaskEngine.State().AddContainer(
				&apicontainer.DockerContainer{
					DockerID:   container.ID,
					DockerName: "gremlin",
					Container:  testTask.Containers[0],
				},
				testTask)

			// Simulate container start prior to listener initialization.
			time.Sleep(checkPointSleep)
			err = engine.MustInit(ctx, taskEngine, defaultCluster, defaultContainerInstance)
			require.NoError(t, err, "initializing stats engine failed")
			serviceConnectStats, err := newServiceConnectStats()
			require.NoError(t, err, "expected no error")
			engine.taskToServiceConnectStats[taskArn] = serviceConnectStats
			assert.Equal(t, 1, len(engine.taskToServiceConnectStats))

			defer engine.containerChangeEventStream.Unsubscribe(containerChangeHandler)

			// simulate appnet server providing service connect metrics
			r := mux.NewRouter()
			r.HandleFunc(testStatsRestPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				fmt.Fprintf(w, "%v", testStats)
			}))
			ts := httptest.NewUnstartedServer(r)
			l, err := net.Listen("unix", testUDSPath)
			require.NoError(t, err)

			ts.Listener.Close()
			ts.Listener = l
			ts.Start()
			defer ts.Close()

			// Wait for the stats collection go routine to start.
			time.Sleep(checkPointSleep)
			if tc.shouldDisableMetrics {
				validateInstanceMetricsWithDisabledMetrics(t, engine, true)
			} else {
				validateInstanceMetrics(t, engine, true)
			}
			scStats := engine.taskToServiceConnectStats[taskArn]
			require.True(t, scStats.sent, "expected service connect metrics sent flag to be set")
			validateEmptyTaskHealthMetrics(t, engine)

			err = client.ContainerStop(ctx, container.ID, containerOptions)
			require.NoError(t, err, "stopping container failed")

			err = engine.containerChangeEventStream.WriteToEventStream(dockerapi.DockerContainerChangeEvent{
				Status: apicontainerstatus.ContainerStopped,
				DockerContainerMetadata: dockerapi.DockerContainerMetadata{
					DockerID: container.ID,
				},
			})
			assert.NoError(t, err, "failed to write to container change event stream")

			time.Sleep(waitForCleanupSleep)

			// Should not contain any metrics after cleanup.
			if !tc.shouldDisableMetrics {
				validateIdleContainerMetrics(t, engine)
			}
			validateEmptyTaskHealthMetrics(t, engine)
		})
	}
}
