package devops

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/stretchr/testify/require"
)

func TestClientGetsTenant(t *testing.T) {
	t.Parallel()

	client := newTestClient(t, func(r *http.Request) any {
		require.Equal(t, "/api/v1/devops/tenants/tenant-1", r.URL.Path)
		return map[string]any{
			"id":     "tenant-1",
			"name":   "Tenant One",
			"region": "APAC",
		}
	})

	tenant, err := client.GetTenant(context.Background(), "tenant-1")

	require.NoError(t, err)
	require.Equal(t, "tenant-1", tenant.ID)
	require.Equal(t, "Tenant One", tenant.Name)
	require.Equal(t, "APAC", tenant.Region)
}

func TestClientGetsClusterMetadata(t *testing.T) {
	t.Parallel()

	client := newTestClient(t, func(r *http.Request) any {
		require.Equal(t, "/api/v1/devops/clusters/cluster-1", r.URL.Path)
		return map[string]any{
			"id":          "cluster-1",
			"name":        "Cluster One",
			"deploy_type": "DEPLOY_TYPE_NEXTGEN",
			"provider":    "aws",
			"region":      "us-west-2",
			"tenant_id":   "tenant-1",
		}
	})

	cluster, err := client.GetCluster(context.Background(), "cluster-1")

	require.NoError(t, err)
	require.Equal(t, "cluster-1", cluster.ID)
	require.Equal(t, "Cluster One", cluster.Name)
	require.Equal(t, "DEPLOY_TYPE_NEXTGEN", cluster.DeployType)
	require.Equal(t, "aws", cluster.Provider)
	require.Equal(t, "us-west-2", cluster.Region)
	require.Equal(t, "tenant-1", cluster.TenantID)
}

func TestClientListsOperationsAcrossPagesWithinWindow(t *testing.T) {
	t.Parallel()

	start := time.Date(2026, 5, 18, 1, 0, 0, 0, time.UTC)
	end := start.Add(2 * time.Hour)
	calls := 0
	client := newTestClient(t, func(r *http.Request) any {
		require.Equal(t, "/api/v1/devops/operation/instances", r.URL.Path)
		require.Equal(t, "cluster", r.URL.Query().Get("type"))
		require.Equal(t, "cluster-1", r.URL.Query().Get("target_id"))
		require.Equal(t, strconv.FormatInt(start.Unix(), 10), r.URL.Query().Get("create_after"))
		require.Empty(t, r.URL.Query().Get("create_before"))
		require.Equal(t, "100", r.URL.Query().Get("per_page"))

		calls++
		switch r.URL.Query().Get("page") {
		case "1":
			return map[string]any{
				"total": 200,
				"items": []map[string]any{
					{"id": 101, "operation": "op-after", "phase": "succeeded", "type": "cluster", "created_at": end.Add(time.Second).Unix()},
					{"id": 102, "operation": "op-in-1", "phase": "succeeded", "type": "cluster", "created_at": start.Add(time.Minute).Unix()},
				},
			}
		case "2":
			return map[string]any{
				"total": 200,
				"items": []map[string]any{
					{"id": 103, "operation": "op-in-2", "phase": "succeeded", "type": "cluster", "created_at": start.Add(30 * time.Minute).Unix()},
					{"id": 104, "operation": "op-before", "phase": "succeeded", "type": "cluster", "created_at": start.Add(-time.Second).Unix()},
				},
			}
		default:
			t.Fatalf("unexpected page %q", r.URL.Query().Get("page"))
			return nil
		}
	})

	operations, err := client.ListOperations(context.Background(), "cluster-1", TimeWindow{Start: start, End: end})

	require.NoError(t, err)
	require.Equal(t, 2, calls)
	require.Equal(t, []string{"102", "103"}, operationIDs(operations))
}

func TestClientStopsOperationsAfterDescendingPagePassesWindowStart(t *testing.T) {
	t.Parallel()

	start := time.Date(2026, 5, 18, 1, 0, 0, 0, time.UTC)
	end := start.Add(2 * time.Hour)
	calls := 0
	client := newTestClient(t, func(r *http.Request) any {
		calls++
		switch r.URL.Query().Get("page") {
		case "1":
			return map[string]any{
				"total": 1000,
				"items": []map[string]any{
					{"id": 201, "operation": "op-after-1", "phase": "succeeded", "type": "cluster", "created_at": end.Add(time.Hour).Unix()},
					{"id": 202, "operation": "op-after-2", "phase": "succeeded", "type": "cluster", "created_at": end.Add(time.Minute).Unix()},
				},
			}
		case "2":
			return map[string]any{
				"total": 1000,
				"items": []map[string]any{
					{"id": 203, "operation": "op-in", "phase": "succeeded", "type": "cluster", "created_at": start.Add(time.Hour).Unix()},
					{"id": 204, "operation": "op-before", "phase": "succeeded", "type": "cluster", "created_at": start.Add(-time.Second).Unix()},
				},
			}
		default:
			t.Fatalf("expected early stop before page %q", r.URL.Query().Get("page"))
			return nil
		}
	})

	operations, err := client.ListOperations(context.Background(), "cluster-1", TimeWindow{Start: start, End: end})

	require.NoError(t, err)
	require.Equal(t, 2, calls)
	require.Equal(t, []string{"203"}, operationIDs(operations))
}

func TestClientListsConfigChangesWithinWindow(t *testing.T) {
	t.Parallel()

	start := time.Date(2026, 5, 18, 1, 0, 0, 0, time.UTC)
	end := start.Add(2 * time.Hour)
	client := newTestClient(t, func(r *http.Request) any {
		require.Equal(t, "/api/v1/devops/cloudconfig/cluster/changes", r.URL.Path)
		require.Equal(t, "cluster-1", r.URL.Query().Get("cluster_id"))
		require.Equal(t, "LOGICAL_CLUSTER", r.URL.Query().Get("cluster_type"))
		require.Equal(t, "false", r.URL.Query().Get("id_only"))
		require.Equal(t, "100", r.URL.Query().Get("per_page"))
		require.Equal(t, "1", r.URL.Query().Get("page"))
		return map[string]any{
			"total": 3,
			"items": []map[string]any{
				{"id": 301, "phase": "applied", "reason": "in", "created_at": start.Add(time.Hour).Unix(), "target_cluster": map[string]any{"cluster_id": "cluster-1", "cluster_type": "LOGICAL_CLUSTER"}},
				{"id": 302, "phase": "applied", "reason": "before", "created_at": start.Add(-time.Second).Unix(), "target_cluster": map[string]any{"cluster_id": "cluster-1", "cluster_type": "LOGICAL_CLUSTER"}},
				{"id": 303, "phase": "applied", "reason": "after", "created_at": end.Add(time.Second).Unix(), "target_cluster": map[string]any{"cluster_id": "cluster-1", "cluster_type": "LOGICAL_CLUSTER"}},
			},
		}
	})

	changes, err := client.ListConfigChanges(context.Background(), "cluster-1", "LOGICAL_CLUSTER", TimeWindow{Start: start, End: end})

	require.NoError(t, err)
	require.Equal(t, []string{"301"}, configChangeIDs(changes))
}

func TestClientAcceptsStringErrCodeInListResponse(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 5, 21, 14, 0, 0, 0, time.UTC)
	client := newTestClient(t, func(r *http.Request) any {
		require.Equal(t, "/api/v1/devops/operation/instances", r.URL.Path)
		return map[string]any{
			"errcode": "OK",
			"errmsg":  "",
			"total":   1,
			"items": []map[string]any{
				{"id": 401, "operation": "upgrade", "phase": "succeeded", "type": "cluster", "created_at": now.Unix()},
			},
		}
	})

	operations, err := client.ListOperations(context.Background(), "cluster-1", TimeWindow{})

	require.NoError(t, err)
	require.Equal(t, []string{"401"}, operationIDs(operations))
}

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

func newTestClient(t *testing.T, handler func(r *http.Request) any) Client {
	t.Helper()

	restyClient := resty.New().
		SetBaseURL("http://devops.test").
		SetTransport(roundTripFunc(func(req *http.Request) (*http.Response, error) {
			payload := handler(req)
			data, err := json.Marshal(payload)
			require.NoError(t, err)
			return &http.Response{
				StatusCode: http.StatusOK,
				Status:     "200 OK",
				Header:     http.Header{"Content-Type": []string{"application/json"}},
				Body:       io.NopCloser(strings.NewReader(string(data))),
				Request:    req,
			}, nil
		}))
	return &httpClient{resty: restyClient, frontendURL: "https://ops.example.test"}
}

func operationIDs(operations []*Operation) []string {
	ids := make([]string, 0, len(operations))
	for _, operation := range operations {
		ids = append(ids, operation.ID)
	}
	return ids
}

func configChangeIDs(changes []*ConfigChange) []string {
	ids := make([]string, 0, len(changes))
	for _, change := range changes {
		ids = append(ids, change.ID)
	}
	return ids
}
