package endpoints

import (
	"strconv"
	"testing"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/datastore"
	"github.com/portainer/portainer/api/internal/testhelpers"
	"github.com/portainer/portainer/api/slicesx"

	"github.com/stretchr/testify/assert"
)

type filterTest struct {
	title    string
	expected []portainer.EndpointID
	query    EnvironmentsQuery
}

func Test_Filter_AgentVersion(t *testing.T) {

	version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
		Type: portainer.AgentOnDockerEnvironment,
		Agent: struct {
			Version string `example:"1.0.0"`
		}{Version: "1.0.0"}}
	version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
		Type: portainer.AgentOnDockerEnvironment,
		Agent: struct {
			Version string `example:"1.0.0"`
		}{Version: "2.0.0"}}
	noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
		Type: portainer.AgentOnDockerEnvironment,
	}
	notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}

	endpoints := []portainer.Endpoint{
		version1Endpoint,
		version2Endpoint,
		noVersionEndpoint,
		notAgentEnvironments,
	}

	handler := setupFilterTest(t, endpoints)

	tests := []filterTest{
		{
			"should show version 1 endpoints",
			[]portainer.EndpointID{version1Endpoint.ID},
			EnvironmentsQuery{
				agentVersions: []string{version1Endpoint.Agent.Version},
				types:         []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
			},
		},
		{
			"should show version 2 endpoints",
			[]portainer.EndpointID{version2Endpoint.ID},
			EnvironmentsQuery{
				agentVersions: []string{version2Endpoint.Agent.Version},
				types:         []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
			},
		},
		{
			"should show version 1 and 2 endpoints",
			[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
			EnvironmentsQuery{
				agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
				types:         []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
			},
		},
	}

	runTests(tests, t, handler, endpoints)
}

func Test_Filter_edgeFilter(t *testing.T) {

	trustedEdgeAsync := portainer.Endpoint{ID: 1, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
	untrustedEdgeAsync := portainer.Endpoint{ID: 2, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
	regularUntrustedEdgeStandard := portainer.Endpoint{ID: 3, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
	regularTrustedEdgeStandard := portainer.Endpoint{ID: 4, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
	regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}

	endpoints := []portainer.Endpoint{
		trustedEdgeAsync,
		untrustedEdgeAsync,
		regularUntrustedEdgeStandard,
		regularTrustedEdgeStandard,
		regularEndpoint,
	}

	handler := setupFilterTest(t, endpoints)

	tests := []filterTest{
		{
			"should show all edge endpoints except of the untrusted edge",
			[]portainer.EndpointID{trustedEdgeAsync.ID, regularTrustedEdgeStandard.ID},
			EnvironmentsQuery{
				types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
			},
		},
		{
			"should show only trusted edge devices and other regular endpoints",
			[]portainer.EndpointID{trustedEdgeAsync.ID, regularEndpoint.ID},
			EnvironmentsQuery{
				edgeAsync: BoolAddr(true),
			},
		},
		{
			"should show only untrusted edge devices and other regular endpoints",
			[]portainer.EndpointID{untrustedEdgeAsync.ID, regularEndpoint.ID},
			EnvironmentsQuery{
				edgeAsync:           BoolAddr(true),
				edgeDeviceUntrusted: true,
			},
		},
		{
			"should show no edge devices",
			[]portainer.EndpointID{regularEndpoint.ID, regularTrustedEdgeStandard.ID},
			EnvironmentsQuery{
				edgeAsync: BoolAddr(false),
			},
		},
	}

	runTests(tests, t, handler, endpoints)
}

func Test_Filter_excludeIDs(t *testing.T) {
	ids := []portainer.EndpointID{1, 2, 3, 4, 5, 6, 7, 8, 9}

	environments := slicesx.Map(ids, func(id portainer.EndpointID) portainer.Endpoint {
		return portainer.Endpoint{ID: id, GroupID: 1, Type: portainer.DockerEnvironment}
	})

	handler := setupFilterTest(t, environments)

	tests := []filterTest{
		{
			title:    "should exclude IDs 2,5,8",
			expected: []portainer.EndpointID{1, 3, 4, 6, 7, 9},
			query: EnvironmentsQuery{
				excludeIds: []portainer.EndpointID{2, 5, 8},
			},
		},
	}

	runTests(tests, t, handler, environments)
}

func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
	n := 10000

	endpointIDs := []portainer.EndpointID{}

	endpoints := []portainer.Endpoint{}
	for i := range n {
		endpoints = append(endpoints, portainer.Endpoint{
			ID:      portainer.EndpointID(i + 1),
			Name:    "endpoint-" + strconv.Itoa(i+1),
			GroupID: 1,
			TagIDs:  []portainer.TagID{1},
			Type:    portainer.EdgeAgentOnDockerEnvironment,
		})

		endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
	}

	endpointGroups := []portainer.EndpointGroup{}

	edgeGroups := []portainer.EdgeGroup{}
	for i := range 1000 {
		edgeGroups = append(edgeGroups, portainer.EdgeGroup{
			ID:           portainer.EdgeGroupID(i + 1),
			Name:         "edge-group-" + strconv.Itoa(i+1),
			Endpoints:    append([]portainer.EndpointID{}, endpointIDs...),
			Dynamic:      true,
			TagIDs:       []portainer.TagID{1, 2, 3},
			PartialMatch: true,
		})
	}

	tagsMap := map[portainer.TagID]string{}
	for i := range 10 {
		tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
	}

	searchString := "edge-group"

	b.ResetTimer()

	for range b.N {
		e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
		if len(e) != n {
			b.FailNow()
		}
	}
}

func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
	n := 10000

	endpointIDs := []portainer.EndpointID{}

	endpoints := []portainer.Endpoint{}
	for i := range n {
		endpoints = append(endpoints, portainer.Endpoint{
			ID:      portainer.EndpointID(i + 1),
			Name:    "endpoint-" + strconv.Itoa(i+1),
			GroupID: 1,
			TagIDs:  []portainer.TagID{1, 2, 3},
			Type:    portainer.EdgeAgentOnDockerEnvironment,
		})

		endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
	}

	endpointGroups := []portainer.EndpointGroup{}

	edgeGroups := []portainer.EdgeGroup{}
	for i := range 1000 {
		edgeGroups = append(edgeGroups, portainer.EdgeGroup{
			ID:        portainer.EdgeGroupID(i + 1),
			Name:      "edge-group-" + strconv.Itoa(i+1),
			Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
			Dynamic:   true,
			TagIDs:    []portainer.TagID{1},
		})
	}

	tagsMap := map[portainer.TagID]string{}
	for i := range 10 {
		tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
	}

	searchString := "edge-group"

	b.ResetTimer()

	for range b.N {
		e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
		if len(e) != n {
			b.FailNow()
		}
	}
}

func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
	for _, test := range tests {
		t.Run(test.title, func(t *testing.T) {
			runTest(t, test, handler, append([]portainer.Endpoint{}, endpoints...))
		})
	}
}

func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
	is := assert.New(t)

	filteredEndpoints, _, err := handler.filterEndpointsByQuery(
		endpoints,
		test.query,
		[]portainer.EndpointGroup{},
		[]portainer.EdgeGroup{},
		&portainer.Settings{},
	)

	is.NoError(err)

	is.Equal(len(test.expected), len(filteredEndpoints))

	respIds := []portainer.EndpointID{}

	for _, endpoint := range filteredEndpoints {
		respIds = append(respIds, endpoint.ID)
	}

	is.ElementsMatch(test.expected, respIds)

}

func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) *Handler {
	is := assert.New(t)
	_, store := datastore.MustNewTestStore(t, true, true)

	for _, endpoint := range endpoints {
		err := store.Endpoint().Create(&endpoint)
		is.NoError(err, "error creating environment")
	}

	err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
	is.NoError(err, "error creating a user")

	bouncer := testhelpers.NewTestRequestBouncer()
	handler := NewHandler(bouncer)
	handler.DataStore = store
	handler.ComposeStackManager = testhelpers.NewComposeStackManager()

	return handler
}
