// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package lunatask_test

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	lunatask "git.secluded.site/go-lunatask"
)

const testToken = "test-token"

// clientCall is a function that calls a client method and returns an error.
type clientCall func(*lunatask.Client) error

// testErrorCases runs standard error case tests (401, 404, 500) for a client method.
func testErrorCases(t *testing.T, call clientCall) {
	t.Helper()

	cases := []struct {
		name   string
		status int
	}{
		{"unauthorized", http.StatusUnauthorized},
		{"not_found", http.StatusNotFound},
		{"server_error", http.StatusInternalServerError},
	}

	for _, errorCase := range cases {
		t.Run(errorCase.name, func(t *testing.T) {
			t.Parallel()

			server := newStatusServer(errorCase.status)
			defer server.Close()

			client := lunatask.NewClient(testToken, lunatask.BaseURL(server.URL))
			if err := call(client); err == nil {
				t.Errorf("expected error for %d, got nil", errorCase.status)
			}
		})
	}
}

// newStatusServer returns a server that responds with the given status code.
func newStatusServer(status int) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(status)
	}))
}

// newJSONServer returns a server that verifies GET method, path, auth and responds with JSON.
func newJSONServer(t *testing.T, wantPath string, body string) *httptest.Server {
	t.Helper()

	return httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
		if req.Method != http.MethodGet {
			t.Errorf("Method = %s, want GET", req.Method)
		}

		if req.URL.Path != wantPath {
			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
		}

		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
		}

		writer.WriteHeader(http.StatusOK)

		if _, err := writer.Write([]byte(body)); err != nil {
			t.Errorf("write response: %v", err)
		}
	}))
}

func ptr[T any](v T) *T {
	return &v
}

func ctx() context.Context {
	return context.Background()
}

// requestCapture holds captured request data from a mock server.
type requestCapture struct {
	Body map[string]any
}

// newBodyServer returns a server that verifies method, path, auth, Content-Type
// and captures the request body.
func newBodyServer(
	t *testing.T, wantMethod, wantPath, responseBody string,
) (*httptest.Server, *requestCapture) {
	t.Helper()

	capture := &requestCapture{Body: nil}

	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
		if req.Method != wantMethod {
			t.Errorf("Method = %s, want %s", req.Method, wantMethod)
		}

		if req.URL.Path != wantPath {
			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
		}

		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
		}

		if contentType := req.Header.Get("Content-Type"); contentType != "application/json" {
			t.Errorf("Content-Type = %q, want application/json", contentType)
		}

		body, _ := io.ReadAll(req.Body)
		_ = json.Unmarshal(body, &capture.Body)

		writer.WriteHeader(http.StatusOK)

		if _, err := writer.Write([]byte(responseBody)); err != nil {
			t.Errorf("write response: %v", err)
		}
	}))

	return server, capture
}

// newPOSTServer returns a server that verifies POST method, path, auth and captures request body.
func newPOSTServer(t *testing.T, wantPath, responseBody string) (*httptest.Server, *requestCapture) {
	t.Helper()

	return newBodyServer(t, http.MethodPost, wantPath, responseBody)
}

// newPUTServer returns a server that verifies PUT method, path, auth and captures request body.
func newPUTServer(t *testing.T, wantPath, responseBody string) (*httptest.Server, *requestCapture) {
	t.Helper()

	return newBodyServer(t, http.MethodPut, wantPath, responseBody)
}

// newDELETEServer returns a server that verifies DELETE method, path, auth and responds with JSON.
func newDELETEServer(t *testing.T, wantPath, responseBody string) *httptest.Server {
	t.Helper()

	return httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
		if req.Method != http.MethodDelete {
			t.Errorf("Method = %s, want DELETE", req.Method)
		}

		if req.URL.Path != wantPath {
			t.Errorf("Path = %s, want %s", req.URL.Path, wantPath)
		}

		if auth := req.Header.Get("Authorization"); auth != "bearer "+testToken {
			t.Errorf("Authorization = %q, want %q", auth, "bearer "+testToken)
		}

		writer.WriteHeader(http.StatusOK)

		if _, err := writer.Write([]byte(responseBody)); err != nil {
			t.Errorf("write response: %v", err)
		}
	}))
}

// assertBodyField checks that a JSON body contains the expected string value.
func assertBodyField(t *testing.T, body map[string]any, key, want string) {
	t.Helper()

	if body[key] != want {
		t.Errorf("body.%s = %v, want %q", key, body[key], want)
	}
}

// assertBodyFieldFloat checks that a JSON body contains the expected float64 value.
// JSON unmarshals numbers to float64.
func assertBodyFieldFloat(t *testing.T, body map[string]any, key string, want float64) {
	t.Helper()

	if body[key] != want {
		t.Errorf("body.%s = %v, want %v", key, body[key], want)
	}
}

// filterTest describes a test case for source filtering.
type filterTest struct {
	Name      string
	Source    *string
	SourceID  *string
	WantQuery url.Values
}

// runFilterTests runs source filter tests against a list endpoint.
func runFilterTests(
	t *testing.T, path, emptyResponse string, tests []filterTest, callAPI func(*lunatask.Client, *string, *string) error,
) {
	t.Helper()

	for _, testCase := range tests {
		t.Run(testCase.Name, func(t *testing.T) {
			t.Parallel()

			var gotQuery url.Values

			server := newJSONServer(t, path, emptyResponse)

			server.Config.Handler = http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
				gotQuery = req.URL.Query()

				writer.WriteHeader(http.StatusOK)
				_, _ = writer.Write([]byte(emptyResponse))
			})
			defer server.Close()

			client := lunatask.NewClient(testToken, lunatask.BaseURL(server.URL))

			if err := callAPI(client, testCase.Source, testCase.SourceID); err != nil {
				t.Fatalf("error = %v", err)
			}

			for key, want := range testCase.WantQuery {
				if got := gotQuery.Get(key); got != want[0] {
					t.Errorf("query[%s] = %q, want %q", key, got, want[0])
				}
			}
		})
	}
}
