//go:build linux
// +build linux

// 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 appnet

import (
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/gorilla/mux"
	prometheus "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/assert"
)

const (
	testUDSPath  = "/tmp/appnet_admin.sock"
	testStatsUrl = "http://thingie/stats/are/cool?true&key=value"
	testDrainUrl = "http://widget/drain/connections?all_of_them&key=value"
)

var (
	typeCounter      = prometheus.MetricType_COUNTER
	typeHistogram    = prometheus.MetricType_HISTOGRAM
	mockedValidStats = map[string]*prometheus.MetricFamily{
		"MetricFamily1": {
			Name: aws.String("MetricFamily1"),
			Type: &typeCounter,
			Metric: []*prometheus.Metric{
				{
					Label: []*prometheus.LabelPair{
						{
							Name:  aws.String("dimensionA"),
							Value: aws.String("value1"),
						},
						{
							Name:  aws.String("dimensionB"),
							Value: aws.String("value2"),
						},
					},
					Counter: &prometheus.Counter{
						Value: aws.Float64(1),
					},
				},
			},
		},
		"MetricFamily2": {
			Name: aws.String("MetricFamily2"),
			Type: &typeHistogram,
			Metric: []*prometheus.Metric{
				{
					Label: []*prometheus.LabelPair{
						{
							Name:  aws.String("dimensionX"),
							Value: aws.String("value1"),
						},
						{
							Name:  aws.String("dimensionY"),
							Value: aws.String("value2"),
						},
					},
					Histogram: &prometheus.Histogram{
						Bucket: []*prometheus.Bucket{
							{
								CumulativeCount: aws.Uint64(1),
								UpperBound:      aws.Float64(0.5),
							},
						},
					},
				},
			},
		},
	}
)

func setupTestUdsServer(t *testing.T, rawResponse string) *httptest.Server {
	t.Helper()
	r := mux.NewRouter()
	r.HandleFunc("/stats/are/cool", func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodGet, r.Method)
		assert.True(t, r.URL.Query().Has("true"))
		assert.Equal(t, "value", r.URL.Query().Get("key"))
		fmt.Fprintf(w, "%s", rawResponse)
	})
	r.HandleFunc("/drain/connections", func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodGet, r.Method)
		assert.True(t, r.URL.Query().Has("all_of_them"))
		assert.Equal(t, "value", r.URL.Query().Get("key"))
		fmt.Fprintf(w, "%s", rawResponse)
	})
	ts := httptest.NewUnstartedServer(r)
	l, err := net.Listen("unix", testUDSPath)
	if err != nil {
		t.Fatal("Error setting up test UDS HTTP server", err)
	}
	ts.Listener.Close()
	ts.Listener = l
	ts.Start()
	return ts
}

func TestGetStats(t *testing.T) {
	for _, tc := range []struct {
		name                  string
		udsPath               string
		rawStats              string
		expectedResult        map[string]*prometheus.MetricFamily
		isErrorExpected       bool
		expectedErrorContains string
	}{
		{
			name:    "happy case with valid counter and histogram metrics",
			udsPath: testUDSPath,
			rawStats: `# TYPE MetricFamily1 counter
			MetricFamily1{dimensionA="value1", dimensionB="value2"} 1
			# TYPE MetricFamily2 histogram
			MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1
			`,
			expectedResult:  mockedValidStats,
			isErrorExpected: false,
		},
		{
			name:    "sad case with invalid metrics",
			udsPath: testUDSPath,
			rawStats: `# TYPE MetricFamily1 counter
			bad metric 1
			# TYPE MetricFamily2 histogram
			bad metric 2
			`,
			expectedResult:        nil,
			isErrorExpected:       true,
			expectedErrorContains: "text format parsing error",
		},
		{
			name:    "invalid UDS path",
			udsPath: "/this/doesnt/exist.sock",
			rawStats: `# TYPE MetricFamily1 counter
			MetricFamily1{dimensionA="value1", dimensionB="value2"} 1
			# TYPE MetricFamily2 histogram
			MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1
			`,
			expectedResult:        nil,
			isErrorExpected:       true,
			expectedErrorContains: "dial unix /this/doesnt/exist.sock: connect: no such file or directory",
		},
		{
			name:    "blank UDS path",
			udsPath: "",
			rawStats: `# TYPE MetricFamily1 counter
			MetricFamily1{dimensionA="value1", dimensionB="value2"} 1
			# TYPE MetricFamily2 histogram
			MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1
			`,
			expectedResult:        nil,
			isErrorExpected:       true,
			expectedErrorContains: "appnet client: Path to appnet admin socket was blank",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ts := setupTestUdsServer(t, tc.rawStats)
			t.Cleanup(func() {
				ts.Close()
			})
			stats, err := CreateClient().GetStats(tc.udsPath, testStatsUrl)
			assert.Equal(t, tc.expectedResult, stats)
			if tc.isErrorExpected {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrorContains)
			} else {
				assert.NoError(t, err)
			}
		})
	}

}

func TestDrainInboundConnections(t *testing.T) {
	for _, tc := range []struct {
		name                  string
		udsPath               string
		isErrorExpected       bool
		expectedErrorContains string
	}{
		{
			name:            "happy case with valid uds path",
			udsPath:         testUDSPath,
			isErrorExpected: false,
		},
		{
			name:                  "sad case with invalid uds path",
			udsPath:               "/this/doesnt/exist.sock",
			isErrorExpected:       true,
			expectedErrorContains: "dial unix /this/doesnt/exist.sock: connect: no such file or directory",
		},
		{
			name:                  "sad case with blank uds path",
			udsPath:               "",
			isErrorExpected:       true,
			expectedErrorContains: "appnet client: Path to appnet admin socket was blank",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ts := setupTestUdsServer(t, "not important")
			t.Cleanup(func() {
				ts.Close()
			})
			err := CreateClient().DrainInboundConnections(tc.udsPath, testDrainUrl)
			if tc.isErrorExpected {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrorContains)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
