package endpointedge

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/apikey"
	"github.com/portainer/portainer/api/chisel"
	"github.com/portainer/portainer/api/datastore"
	"github.com/portainer/portainer/api/filesystem"
	"github.com/portainer/portainer/api/http/security"
	"github.com/portainer/portainer/api/jwt"

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

type endpointTestCase struct {
	endpoint           portainer.Endpoint
	endpointRelation   portainer.EndpointRelation
	expectedStatusCode int
}

var endpointTestCases = []endpointTestCase{
	{
		portainer.Endpoint{},
		portainer.EndpointRelation{},
		http.StatusNotFound,
	},
	{
		portainer.Endpoint{
			ID:     -1,
			Name:   "endpoint-id--1",
			Type:   portainer.EdgeAgentOnDockerEnvironment,
			URL:    "https://portainer.io:9443",
			EdgeID: "edge-id",
		},
		portainer.EndpointRelation{
			EndpointID: -1,
		},
		http.StatusNotFound,
	},
	{
		portainer.Endpoint{
			ID:     2,
			Name:   "endpoint-id-2",
			Type:   portainer.EdgeAgentOnDockerEnvironment,
			URL:    "https://portainer.io:9443",
			EdgeID: "",
		},
		portainer.EndpointRelation{
			EndpointID: 2,
		},
		http.StatusForbidden,
	},
	{
		portainer.Endpoint{
			ID:     4,
			Name:   "endpoint-id-4",
			Type:   portainer.EdgeAgentOnDockerEnvironment,
			URL:    "https://portainer.io:9443",
			EdgeID: "edge-id",
		},
		portainer.EndpointRelation{
			EndpointID: 4,
		},
		http.StatusOK,
	},
}

func setupHandler() (*Handler, func(), error) {
	tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
	if err != nil {
		return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err)
	}

	fs, err := filesystem.NewService(tmpDir, "")
	if err != nil {
		return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err)
	}

	_, store, storeTeardown := datastore.MustNewTestStore(true, true)

	ctx := context.Background()
	shutdownCtx, cancelFn := context.WithCancel(ctx)

	teardown := func() {
		cancelFn()
		storeTeardown()
	}

	jwtService, err := jwt.NewService("1h", store)
	if err != nil {
		teardown()
		return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err)
	}

	apiKeyService := apikey.NewAPIKeyService(nil, nil)

	settings, err := store.Settings().Settings()
	if err != nil {
		teardown()
		return nil, nil, fmt.Errorf("could not create new settings: %w", err)
	}
	settings.TrustOnFirstConnect = true

	err = store.Settings().UpdateSettings(settings)
	if err != nil {
		teardown()
		return nil, nil, fmt.Errorf("could not update settings: %w", err)
	}

	handler := NewHandler(
		security.NewRequestBouncer(store, jwtService, apiKeyService),
		store,
		fs,
		chisel.NewService(store, shutdownCtx),
	)

	handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx)

	return handler, teardown, nil
}

func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) {
	// Avoid setting ID below 0 to generate invalid test cases
	if endpoint.ID <= 0 {
		return nil
	}

	err = handler.DataStore.Endpoint().Create(&endpoint)
	if err != nil {
		return err
	}

	return handler.DataStore.EndpointRelation().Create(&endpointRelation)
}

func TestMissingEdgeIdentifier(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	endpointID := portainer.EndpointID(45)
	err = createEndpoint(handler, portainer.Endpoint{
		ID:     endpointID,
		Name:   "endpoint-id-45",
		Type:   portainer.EdgeAgentOnDockerEnvironment,
		URL:    "https://portainer.io:9443",
		EdgeID: "edge-id",
	}, portainer.EndpointRelation{EndpointID: endpointID})

	if err != nil {
		t.Fatal(err)
	}

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
	if err != nil {
		t.Fatal("request error:", err)
	}

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
	}
}

func TestWithEndpoints(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	for _, test := range endpointTestCases {
		err = createEndpoint(handler, test.endpoint, test.endpointRelation)
		if err != nil {
			t.Fatal(err)
		}

		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
		if err != nil {
			t.Fatal("request error:", err)
		}

		req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
		req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")

		rec := httptest.NewRecorder()
		handler.ServeHTTP(rec, req)

		if rec.Code != test.expectedStatusCode {
			t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
		}
	}
}

func TestLastCheckInDateIncreases(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	endpointID := portainer.EndpointID(56)
	endpoint := portainer.Endpoint{
		ID:              endpointID,
		Name:            "test-endpoint-56",
		Type:            portainer.EdgeAgentOnDockerEnvironment,
		URL:             "https://portainer.io:9443",
		EdgeID:          "edge-id",
		LastCheckInDate: time.Now().Unix(),
	}

	endpointRelation := portainer.EndpointRelation{
		EndpointID: endpoint.ID,
	}

	err = createEndpoint(handler, endpoint, endpointRelation)
	if err != nil {
		t.Fatal(err)
	}

	time.Sleep(1 * time.Second)

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
	if err != nil {
		t.Fatal("request error:", err)
	}
	req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
	req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
	}

	updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
	if err != nil {
		t.Fatal(err)
	}

	assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate)
}

func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	endpointID := portainer.EndpointID(44)
	edgeId := "edge-id"
	endpoint := portainer.Endpoint{
		ID:     endpointID,
		Name:   "test-endpoint-44",
		Type:   portainer.EdgeAgentOnDockerEnvironment,
		URL:    "https://portainer.io:9443",
		EdgeID: "",
	}
	endpointRelation := portainer.EndpointRelation{
		EndpointID: endpoint.ID,
	}

	err = createEndpoint(handler, endpoint, endpointRelation)
	if err != nil {
		t.Fatal(err)
	}

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
	if err != nil {
		t.Fatal("request error:", err)
	}
	req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId)
	req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
	}

	updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
}

func TestEdgeStackStatus(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	endpointID := portainer.EndpointID(7)
	endpoint := portainer.Endpoint{
		ID:              endpointID,
		Name:            "test-endpoint-7",
		Type:            portainer.EdgeAgentOnDockerEnvironment,
		URL:             "https://portainer.io:9443",
		EdgeID:          "edge-id",
		LastCheckInDate: time.Now().Unix(),
	}

	edgeStackID := portainer.EdgeStackID(17)
	edgeStack := portainer.EdgeStack{
		ID:   edgeStackID,
		Name: "test-edge-stack-17",
		Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
			endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID},
		},
		CreationDate:   time.Now().Unix(),
		EdgeGroups:     []portainer.EdgeGroupID{1, 2},
		ProjectPath:    "/project/path",
		EntryPoint:     "entrypoint",
		Version:        237,
		ManifestPath:   "/manifest/path",
		DeploymentType: 1,
	}

	endpointRelation := portainer.EndpointRelation{
		EndpointID: endpoint.ID,
		EdgeStacks: map[portainer.EdgeStackID]bool{
			edgeStack.ID: true,
		},
	}
	handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack)

	err = createEndpoint(handler, endpoint, endpointRelation)
	if err != nil {
		t.Fatal(err)
	}

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
	if err != nil {
		t.Fatal("request error:", err)
	}
	req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
	req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
	}

	var data endpointEdgeStatusInspectResponse
	err = json.NewDecoder(rec.Body).Decode(&data)
	if err != nil {
		t.Fatal("error decoding response:", err)
	}

	assert.Len(t, data.Stacks, 1)
	assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
	assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
}

func TestEdgeJobsResponse(t *testing.T) {
	handler, teardown, err := setupHandler()
	defer teardown()

	if err != nil {
		t.Fatal(err)
	}

	endpointID := portainer.EndpointID(77)
	endpoint := portainer.Endpoint{
		ID:              endpointID,
		Name:            "test-endpoint-77",
		Type:            portainer.EdgeAgentOnDockerEnvironment,
		URL:             "https://portainer.io:9443",
		EdgeID:          "edge-id",
		LastCheckInDate: time.Now().Unix(),
	}

	endpointRelation := portainer.EndpointRelation{
		EndpointID: endpoint.ID,
	}

	err = createEndpoint(handler, endpoint, endpointRelation)
	if err != nil {
		t.Fatal(err)
	}

	path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
	if err != nil {
		t.Fatal(err)
	}

	edgeJobID := portainer.EdgeJobID(35)
	edgeJob := portainer.EdgeJob{
		ID:             edgeJobID,
		Created:        time.Now().Unix(),
		CronExpression: "* * * * *",
		Name:           "test-edge-job",
		ScriptPath:     path,
		Recurring:      true,
		Version:        57,
	}

	handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)

	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
	if err != nil {
		t.Fatal("request error:", err)
	}
	req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
	req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")

	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
	}

	var data endpointEdgeStatusInspectResponse
	err = json.NewDecoder(rec.Body).Decode(&data)
	if err != nil {
		t.Fatal("error decoding response:", err)
	}

	assert.Len(t, data.Schedules, 1)
	assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
	assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
	assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
}
