package actionssummerwindnet

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"sync"
	"testing"
	"time"

	actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.summerwind.net/v1alpha1"
	"github.com/go-logr/logr"
	"github.com/google/go-github/v52/github"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

var (
	sc = runtime.NewScheme()
)

func init() {
	_ = clientgoscheme.AddToScheme(sc)
	_ = actionsv1alpha1.AddToScheme(sc)
}

func TestWebhookPing(t *testing.T) {
	testServer(t,
		"ping",
		&github.PingEvent{
			Zen: github.String("zen"),
		},
		200,
		"pong",
	)
}

func TestWebhookWorkflowJob(t *testing.T) {
	setupTest := func() github.WorkflowJobEvent {
		f, err := os.Open("testdata/org_webhook_workflow_job_payload.json")
		if err != nil {
			t.Fatalf("could not open the fixture: %s", err)
		}
		defer f.Close()
		var e github.WorkflowJobEvent
		if err := json.NewDecoder(f).Decode(&e); err != nil {
			t.Fatalf("invalid json: %s", err)
		}

		return e
	}
	t.Run("Successful", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"label1"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"scaled test-name by 1",
			initObjs,
		)
	})
	t.Run("WrongLabels", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"bad-label"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"no horizontalrunnerautoscaler to scale for this github event",
			initObjs,
		)
	})
	// This test verifies that the old way of matching labels doesn't work anymore
	t.Run("OldLabels", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Labels: map[string]string{
							"label1": "label1",
						},
					},
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"bad-label"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"no horizontalrunnerautoscaler to scale for this github event",
			initObjs,
		)
	})
}

func TestWebhookWorkflowJobWithSelfHostedLabel(t *testing.T) {
	setupTest := func() github.WorkflowJobEvent {
		f, err := os.Open("testdata/org_webhook_workflow_job_with_self_hosted_label_payload.json")
		if err != nil {
			t.Fatalf("could not open the fixture: %s", err)
		}
		defer f.Close()
		var e github.WorkflowJobEvent
		if err := json.NewDecoder(f).Decode(&e); err != nil {
			t.Fatalf("invalid json: %s", err)
		}

		return e
	}
	t.Run("Successful", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"label1"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"scaled test-name by 1",
			initObjs,
		)
	})
	t.Run("WrongLabels", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"bad-label"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"no horizontalrunnerautoscaler to scale for this github event",
			initObjs,
		)
	})
	// This test verifies that the old way of matching labels doesn't work anymore
	t.Run("OldLabels", func(t *testing.T) {
		e := setupTest()
		hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
				ScaleTargetRef: actionsv1alpha1.ScaleTargetRef{
					Name: "test-name",
				},
				ScaleUpTriggers: []actionsv1alpha1.ScaleUpTrigger{
					{
						GitHubEvent: &actionsv1alpha1.GitHubEventScaleUpTriggerSpec{
							WorkflowJob: &actionsv1alpha1.WorkflowJobSpec{},
						},
					},
				},
			},
		}

		rd := &actionsv1alpha1.RunnerDeployment{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-name",
			},
			Spec: actionsv1alpha1.RunnerDeploymentSpec{
				Template: actionsv1alpha1.RunnerTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Labels: map[string]string{
							"label1": "label1",
						},
					},
					Spec: actionsv1alpha1.RunnerSpec{
						RunnerConfig: actionsv1alpha1.RunnerConfig{
							Organization: "MYORG",
							Labels:       []string{"bad-label"},
						},
					},
				},
			},
		}

		initObjs := []runtime.Object{hra, rd}

		testServerWithInitObjs(t,
			"workflow_job",
			&e,
			200,
			"no horizontalrunnerautoscaler to scale for this github event",
			initObjs,
		)
	})
}

func TestGetRequest(t *testing.T) {
	hra := HorizontalRunnerAutoscalerGitHubWebhook{}
	request, _ := http.NewRequest(http.MethodGet, "/", nil)
	recorder := httptest.ResponseRecorder{}

	hra.Handle(&recorder, request)
	response := recorder.Result()

	if response.StatusCode != http.StatusOK {
		t.Errorf("want %d, got %d", http.StatusOK, response.StatusCode)
	}
}

func TestGetValidCapacityReservations(t *testing.T) {
	now := time.Now()
	duration, _ := time.ParseDuration("10m")
	effectiveTime := now.Add(-duration)

	hra := &actionsv1alpha1.HorizontalRunnerAutoscaler{
		Spec: actionsv1alpha1.HorizontalRunnerAutoscalerSpec{
			CapacityReservations: []actionsv1alpha1.CapacityReservation{
				{
					EffectiveTime:  metav1.Time{Time: effectiveTime.Add(-time.Second)},
					ExpirationTime: metav1.Time{Time: now.Add(-time.Second)},
					Replicas:       1,
				},
				{
					EffectiveTime:  metav1.Time{Time: effectiveTime},
					ExpirationTime: metav1.Time{Time: now},
					Replicas:       2,
				},
				{
					EffectiveTime:  metav1.Time{Time: effectiveTime.Add(time.Second)},
					ExpirationTime: metav1.Time{Time: now.Add(time.Second)},
					Replicas:       3,
				},
			},
		},
	}

	revs := getValidCapacityReservations(hra)

	var count int

	for _, r := range revs {
		count += r.Replicas
	}

	want := 3

	if count != want {
		t.Errorf("want %d, got %d", want, count)
	}
}

func installTestLogger(webhook *HorizontalRunnerAutoscalerGitHubWebhook) *bytes.Buffer {
	logs := &bytes.Buffer{}

	// Wrap the buffer with a synchronized writer to prevent race conditions
	syncWriter := &syncWriter{
		writer: logs,
	}

	sink := &testLogSink{
		name:   "testlog",
		writer: syncWriter,
	}

	log := logr.New(sink)

	webhook.Log = log

	return logs
}

func testServerWithInitObjs(t *testing.T, eventType string, event interface{}, wantCode int, wantBody string, initObjs []runtime.Object) {
	t.Helper()

	hraWebhook := &HorizontalRunnerAutoscalerGitHubWebhook{}

	client := fake.NewClientBuilder().
		WithScheme(sc).
		WithRuntimeObjects(initObjs...).
		WithIndex(&actionsv1alpha1.HorizontalRunnerAutoscaler{}, scaleTargetKey, hraWebhook.indexer).
		Build()

	logs := installTestLogger(hraWebhook)

	defer func() {
		if t.Failed() {
			t.Logf("diagnostics: %s", logs.String())
		}
	}()

	hraWebhook.Client = client

	mux := http.NewServeMux()
	mux.HandleFunc("/", hraWebhook.Handle)

	server := httptest.NewServer(mux)
	defer server.Close()

	resp, err := sendWebhook(server, eventType, event)
	if err != nil {
		t.Fatal(err)
	}

	defer func() {
		if resp != nil {
			resp.Body.Close()
		}
	}()

	if resp.StatusCode != wantCode {
		t.Error("status:", resp.StatusCode)
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}

	if string(respBody) != wantBody {
		t.Fatal("body:", string(respBody))
	}
}

func testServer(t *testing.T, eventType string, event interface{}, wantCode int, wantBody string) {
	var initObjs []runtime.Object
	testServerWithInitObjs(t, eventType, event, wantCode, wantBody, initObjs)
}

func sendWebhook(server *httptest.Server, eventType string, event interface{}) (*http.Response, error) {
	jsonBuf := &bytes.Buffer{}
	enc := json.NewEncoder(jsonBuf)
	enc.SetIndent("  ", "")
	err := enc.Encode(event)
	if err != nil {
		return nil, fmt.Errorf("[bug in test] encoding event to json: %+v", err)
	}

	reqBody := jsonBuf.Bytes()

	u, err := url.Parse(server.URL)
	if err != nil {
		return nil, fmt.Errorf("parsing server url: %v", err)
	}

	req := &http.Request{
		Method: http.MethodPost,
		URL:    u,
		Header: map[string][]string{
			"X-GitHub-Event": {eventType},
			"Content-Type":   {"application/json"},
		},
		Body: io.NopCloser(bytes.NewBuffer(reqBody)),
	}

	return http.DefaultClient.Do(req)
}

// syncWriter wraps an io.Writer with a mutex for thread-safe writes
type syncWriter struct {
	writer io.Writer
	mu     sync.Mutex
}

func (sw *syncWriter) Write(p []byte) (n int, err error) {
	sw.mu.Lock()
	defer sw.mu.Unlock()
	return sw.writer.Write(p)
}

// testLogSink is a sample logr.Logger that logs in-memory.
// It's only for testing log outputs.
type testLogSink struct {
	name      string
	keyValues map[string]interface{}

	writer io.Writer
}

var _ logr.LogSink = &testLogSink{}

func (l *testLogSink) Init(_ logr.RuntimeInfo) {

}

func (l *testLogSink) Info(_ int, msg string, kvs ...interface{}) {
	fmt.Fprintf(l.writer, "%s] %s\t", l.name, msg)
	for k, v := range l.keyValues {
		fmt.Fprintf(l.writer, "%s=%+v ", k, v)
	}
	for i := 0; i < len(kvs); i += 2 {
		fmt.Fprintf(l.writer, "%s=%+v ", kvs[i], kvs[i+1])
	}
	fmt.Fprintf(l.writer, "\n")
}

func (*testLogSink) Enabled(level int) bool {
	return true
}

func (l *testLogSink) Error(err error, msg string, kvs ...interface{}) {
	kvs = append(kvs, "error", err)
	l.Info(0, msg, kvs...)
}

func (l *testLogSink) WithName(name string) logr.LogSink {
	return &testLogSink{
		name:      l.name + "." + name,
		keyValues: l.keyValues,
		writer:    l.writer,
	}
}

func (l *testLogSink) WithValues(kvs ...interface{}) logr.LogSink {
	newMap := make(map[string]interface{}, len(l.keyValues)+len(kvs)/2)
	for k, v := range l.keyValues {
		newMap[k] = v
	}
	for i := 0; i < len(kvs); i += 2 {
		newMap[kvs[i].(string)] = kvs[i+1]
	}
	return &testLogSink{
		name:      l.name,
		keyValues: newMap,
		writer:    l.writer,
	}
}
