package services

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

	"Noooste/garage-ui/internal/config"
	"Noooste/garage-ui/internal/models"
)

// newAdminTestServer wires an httptest.Server (with the supplied handler) to a
// fresh *GarageAdminService configured with a known bearer token.
func newAdminTestServer(t *testing.T, handler http.Handler) (*GarageAdminService, *httptest.Server) {
	t.Helper()
	srv := httptest.NewServer(handler)
	t.Cleanup(srv.Close)

	svc := NewGarageAdminService(&config.GarageConfig{
		AdminEndpoint: srv.URL,
		AdminToken:    "test-token-xyz",
	}, "")
	return svc, srv
}

// jsonHandler returns an http.Handler that writes the supplied status code and
// JSON-encoded body.
func jsonHandler(t *testing.T, status int, body any) http.Handler {
	t.Helper()
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		if body == nil {
			return
		}
		if err := json.NewEncoder(w).Encode(body); err != nil {
			t.Errorf("server: encode response: %v", err)
		}
	})
}

type recordedRequest struct {
	method string
	path   string
	rawURL string
	auth   string
	body   []byte
}

func recordingHandler(t *testing.T, status int, respBody any) (http.Handler, *recordedRequest) {
	t.Helper()
	rec := &recordedRequest{}
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec.method = r.Method
		rec.path = r.URL.Path
		rec.rawURL = r.URL.RequestURI()
		rec.auth = r.Header.Get("Authorization")
		if r.Body != nil {
			b, err := io.ReadAll(r.Body)
			if err != nil {
				t.Errorf("server: read body: %v", err)
			}
			rec.body = b
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(status)
		if respBody != nil {
			_ = json.NewEncoder(w).Encode(respBody)
		}
	})
	return h, rec
}

func TestHealthCheck_Success(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, nil)
	svc, _ := newAdminTestServer(t, h)

	if err := svc.HealthCheck(context.Background()); err != nil {
		t.Fatalf("HealthCheck: %v", err)
	}
	if rec.method != http.MethodGet {
		t.Errorf("method = %q, want GET", rec.method)
	}
	if rec.path != "/health" {
		t.Errorf("path = %q, want /health", rec.path)
	}
	if rec.auth != "Bearer test-token-xyz" {
		t.Errorf("Authorization = %q, want Bearer test-token-xyz", rec.auth)
	}
}

func TestHealthCheck_Non2xxReturnsError(t *testing.T) {
	h := jsonHandler(t, http.StatusServiceUnavailable, map[string]string{"error": "down"})
	svc, _ := newAdminTestServer(t, h)

	err := svc.HealthCheck(context.Background())
	if err == nil {
		t.Fatal("expected error for 503, got nil")
	}
	if !strings.Contains(err.Error(), "503") {
		t.Errorf("error should mention status code, got %v", err)
	}
}

func TestListKeys_Success(t *testing.T) {
	want := []models.ListKeysResponseItem{
		{ID: "k1", Name: "alice", Expired: false},
		{ID: "k2", Name: "bob", Expired: true},
	}
	h, rec := recordingHandler(t, http.StatusOK, want)
	svc, _ := newAdminTestServer(t, h)

	got, err := svc.ListKeys(context.Background())
	if err != nil {
		t.Fatalf("ListKeys: %v", err)
	}
	if rec.method != http.MethodGet || rec.path != "/v2/ListKeys" {
		t.Errorf("request = %s %s, want GET /v2/ListKeys", rec.method, rec.path)
	}
	if len(got) != 2 || got[0].ID != "k1" || got[1].Name != "bob" {
		t.Errorf("got %+v, want %+v", got, want)
	}
}

func TestCreateKey_PostsBodyAndDecodesResponse(t *testing.T) {
	name := "service-account"
	want := &models.GarageKeyInfo{
		AccessKeyID: "GK123",
		Name:        name,
	}
	h, rec := recordingHandler(t, http.StatusOK, want)
	svc, _ := newAdminTestServer(t, h)

	req := models.CreateKeyRequest{Name: &name, NeverExpires: true}
	got, err := svc.CreateKey(context.Background(), req)
	if err != nil {
		t.Fatalf("CreateKey: %v", err)
	}
	if rec.method != http.MethodPost || rec.path != "/v2/CreateKey" {
		t.Errorf("request = %s %s", rec.method, rec.path)
	}
	var sent models.CreateKeyRequest
	if err := json.Unmarshal(rec.body, &sent); err != nil {
		t.Fatalf("unmarshal sent body: %v (raw=%q)", err, rec.body)
	}
	if sent.Name == nil || *sent.Name != name {
		t.Errorf("sent name = %v, want %q", sent.Name, name)
	}
	if !sent.NeverExpires {
		t.Errorf("NeverExpires should round-trip true")
	}
	if got.AccessKeyID != want.AccessKeyID {
		t.Errorf("AccessKeyID = %q, want %q", got.AccessKeyID, want.AccessKeyID)
	}
}

func TestGetKeyInfo_PassesShowSecretQueryParam(t *testing.T) {
	cases := []struct {
		name         string
		showSecret   bool
		wantInRawURL string
	}{
		{"without secret", false, "?id=ABC"},
		{"with secret", true, "?id=ABC&showSecretKey=true"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			h, rec := recordingHandler(t, http.StatusOK, &models.GarageKeyInfo{AccessKeyID: "ABC"})
			svc, _ := newAdminTestServer(t, h)

			_, err := svc.GetKeyInfo(context.Background(), "ABC", tc.showSecret)
			if err != nil {
				t.Fatalf("GetKeyInfo: %v", err)
			}
			if rec.method != http.MethodGet {
				t.Errorf("method = %q, want GET", rec.method)
			}
			if !strings.Contains(rec.rawURL, tc.wantInRawURL) {
				t.Errorf("raw URL = %q, want substring %q", rec.rawURL, tc.wantInRawURL)
			}
		})
	}
}

func TestUpdateKey_UsesIDInQuery(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, &models.GarageKeyInfo{AccessKeyID: "K"})
	svc, _ := newAdminTestServer(t, h)

	if _, err := svc.UpdateKey(context.Background(), "K", models.UpdateKeyRequest{}); err != nil {
		t.Fatalf("UpdateKey: %v", err)
	}
	if rec.method != http.MethodPost {
		t.Errorf("method = %q, want POST", rec.method)
	}
	if !strings.Contains(rec.rawURL, "id=K") {
		t.Errorf("raw URL = %q, want substring id=K", rec.rawURL)
	}
}

func TestDeleteKey_NoBodyOnSuccess(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, nil)
	svc, _ := newAdminTestServer(t, h)

	if err := svc.DeleteKey(context.Background(), "K"); err != nil {
		t.Fatalf("DeleteKey: %v", err)
	}
	if rec.method != http.MethodPost {
		t.Errorf("method = %q, want POST", rec.method)
	}
	if !strings.Contains(rec.rawURL, "id=K") {
		t.Errorf("raw URL = %q", rec.rawURL)
	}
}

func TestImportKey_PostsToImportEndpoint(t *testing.T) {
	want := &models.GarageKeyInfo{AccessKeyID: "imported"}
	h, rec := recordingHandler(t, http.StatusOK, want)
	svc, _ := newAdminTestServer(t, h)

	req := models.ImportKeyRequest{
		AccessKeyID:     "imported",
		SecretAccessKey: "shhh",
	}
	got, err := svc.ImportKey(context.Background(), req)
	if err != nil {
		t.Fatalf("ImportKey: %v", err)
	}
	if rec.path != "/v2/ImportKey" {
		t.Errorf("path = %q", rec.path)
	}
	if got.AccessKeyID != "imported" {
		t.Errorf("AccessKeyID = %q, want imported", got.AccessKeyID)
	}
}

func TestListBuckets_Success(t *testing.T) {
	want := []models.ListBucketsResponseItem{{ID: "b1"}, {ID: "b2"}}
	h, rec := recordingHandler(t, http.StatusOK, want)
	svc, _ := newAdminTestServer(t, h)

	got, err := svc.ListBuckets(context.Background())
	if err != nil {
		t.Fatalf("ListBuckets: %v", err)
	}
	if rec.path != "/v2/ListBuckets" {
		t.Errorf("path = %q", rec.path)
	}
	if len(got) != 2 {
		t.Errorf("len = %d, want 2", len(got))
	}
}

func TestGetBucketInfo_UsesIDQuery(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, &models.GarageBucketInfo{ID: "B"})
	svc, _ := newAdminTestServer(t, h)
	if _, err := svc.GetBucketInfo(context.Background(), "B"); err != nil {
		t.Fatalf("GetBucketInfo: %v", err)
	}
	if !strings.Contains(rec.rawURL, "id=B") {
		t.Errorf("raw URL = %q", rec.rawURL)
	}
}

func TestGetBucketInfoByAlias_UsesGlobalAliasQuery(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, &models.GarageBucketInfo{ID: "B", GlobalAliases: []string{"my-bucket"}})
	svc, _ := newAdminTestServer(t, h)
	got, err := svc.GetBucketInfoByAlias(context.Background(), "my-bucket")
	if err != nil {
		t.Fatalf("GetBucketInfoByAlias: %v", err)
	}
	if !strings.Contains(rec.rawURL, "globalAlias=my-bucket") {
		t.Errorf("raw URL = %q", rec.rawURL)
	}
	if got.ID != "B" {
		t.Errorf("ID = %q, want B", got.ID)
	}
}

func TestCreateBucket_PostsBody(t *testing.T) {
	want := &models.GarageBucketInfo{ID: "new"}
	h, rec := recordingHandler(t, http.StatusOK, want)
	svc, _ := newAdminTestServer(t, h)

	if _, err := svc.CreateBucket(context.Background(), models.CreateBucketAdminRequest{}); err != nil {
		t.Fatalf("CreateBucket: %v", err)
	}
	if rec.method != http.MethodPost || rec.path != "/v2/CreateBucket" {
		t.Errorf("request = %s %s", rec.method, rec.path)
	}
}

func TestUpdateBucket_UsesIDQueryAndPostsBody(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, &models.GarageBucketInfo{ID: "B"})
	svc, _ := newAdminTestServer(t, h)
	if _, err := svc.UpdateBucket(context.Background(), "B", models.UpdateBucketRequest{}); err != nil {
		t.Fatalf("UpdateBucket: %v", err)
	}
	if rec.method != http.MethodPost {
		t.Errorf("method = %q", rec.method)
	}
	if !strings.Contains(rec.rawURL, "id=B") {
		t.Errorf("raw URL = %q", rec.rawURL)
	}
}

func TestDeleteBucket_PostWithIDQuery(t *testing.T) {
	h, rec := recordingHandler(t, http.StatusOK, nil)
	svc, _ := newAdminTestServer(t, h)
	if err := svc.DeleteBucket(context.Background(), "B"); err != nil {
		t.Fatalf("DeleteBucket: %v", err)
	}
	if rec.method != http.MethodPost {
		t.Errorf("method = %q", rec.method)
	}
	if !strings.Contains(rec.rawURL, "id=B") {
		t.Errorf("raw URL = %q", rec.rawURL)
	}
}

func TestBucketAliasAndPermissionEndpoints(t *testing.T) {
	cases := []struct {
		name string
		fn   func(s *GarageAdminService) error
		path string
	}{
		{
			name: "AddBucketAlias",
			fn: func(s *GarageAdminService) error {
				_, err := s.AddBucketAlias(context.Background(), models.AddBucketAliasRequest{})
				return err
			},
			path: "/v2/AddBucketAlias",
		},
		{
			name: "RemoveBucketAlias",
			fn: func(s *GarageAdminService) error {
				_, err := s.RemoveBucketAlias(context.Background(), models.RemoveBucketAliasRequest{})
				return err
			},
			path: "/v2/RemoveBucketAlias",
		},
		{
			name: "AllowBucketKey",
			fn: func(s *GarageAdminService) error {
				_, err := s.AllowBucketKey(context.Background(), models.BucketKeyPermRequest{})
				return err
			},
			path: "/v2/AllowBucketKey",
		},
		{
			name: "DenyBucketKey",
			fn: func(s *GarageAdminService) error {
				_, err := s.DenyBucketKey(context.Background(), models.BucketKeyPermRequest{})
				return err
			},
			path: "/v2/DenyBucketKey",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			h, rec := recordingHandler(t, http.StatusOK, &models.GarageBucketInfo{ID: "B"})
			svc, _ := newAdminTestServer(t, h)
			if err := tc.fn(svc); err != nil {
				t.Fatalf("%s: %v", tc.name, err)
			}
			if rec.path != tc.path || rec.method != http.MethodPost {
				t.Errorf("request = %s %s, want POST %s", rec.method, rec.path, tc.path)
			}
		})
	}
}

func TestClusterEndpoints(t *testing.T) {
	cases := []struct {
		name string
		fn   func(s *GarageAdminService) error
		path string
	}{
		{
			name: "GetClusterHealth",
			fn: func(s *GarageAdminService) error {
				_, err := s.GetClusterHealth(context.Background())
				return err
			},
			path: "/v2/GetClusterHealth",
		},
		{
			name: "GetClusterStatus",
			fn: func(s *GarageAdminService) error {
				_, err := s.GetClusterStatus(context.Background())
				return err
			},
			path: "/v2/GetClusterStatus",
		},
		{
			name: "GetClusterStatistics",
			fn: func(s *GarageAdminService) error {
				_, err := s.GetClusterStatistics(context.Background())
				return err
			},
			path: "/v2/GetClusterStatistics",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			h, rec := recordingHandler(t, http.StatusOK, struct{}{})
			svc, _ := newAdminTestServer(t, h)
			if err := tc.fn(svc); err != nil {
				t.Fatalf("%s: %v", tc.name, err)
			}
			if rec.method != http.MethodGet {
				t.Errorf("method = %q, want GET", rec.method)
			}
			if rec.path != tc.path {
				t.Errorf("path = %q, want %q", rec.path, tc.path)
			}
		})
	}
}

func TestNodeEndpoints_NodeIDInQuery(t *testing.T) {
	cases := []struct {
		name string
		fn   func(s *GarageAdminService, id string) error
		path string
	}{
		{
			name: "GetNodeInfo",
			fn: func(s *GarageAdminService, id string) error {
				_, err := s.GetNodeInfo(context.Background(), id)
				return err
			},
			path: "/v2/GetNodeInfo",
		},
		{
			name: "GetNodeStatistics",
			fn: func(s *GarageAdminService, id string) error {
				_, err := s.GetNodeStatistics(context.Background(), id)
				return err
			},
			path: "/v2/GetNodeStatistics",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			h, rec := recordingHandler(t, http.StatusOK, struct{}{})
			svc, _ := newAdminTestServer(t, h)
			if err := tc.fn(svc, "node-7"); err != nil {
				t.Fatalf("%s: %v", tc.name, err)
			}
			if rec.path != tc.path {
				t.Errorf("path = %q, want %q", rec.path, tc.path)
			}
			if !strings.Contains(rec.rawURL, "node=node-7") {
				t.Errorf("raw URL = %q, want substring node=node-7", rec.rawURL)
			}
		})
	}
}

func TestGetMetrics_ReturnsRawBodyOn2xx(t *testing.T) {
	body := "# HELP garage_up Number of running nodes\ngarage_up 3\n"
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		w.WriteHeader(http.StatusOK)
		_, _ = io.WriteString(w, body)
	})
	svc, _ := newAdminTestServer(t, h)

	got, err := svc.GetMetrics(context.Background())
	if err != nil {
		t.Fatalf("GetMetrics: %v", err)
	}
	if got != body {
		t.Errorf("GetMetrics body mismatch:\n got %q\nwant %q", got, body)
	}
}

func TestGetMetrics_Non2xxReturnsErrorWithStatus(t *testing.T) {
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusForbidden)
		_, _ = io.WriteString(w, "forbidden")
	})
	svc, _ := newAdminTestServer(t, h)

	_, err := svc.GetMetrics(context.Background())
	if err == nil {
		t.Fatal("expected error for 403, got nil")
	}
	if !strings.Contains(err.Error(), "403") {
		t.Errorf("error should mention 403, got %v", err)
	}
}

func TestDoRequest_Non2xxBodyEchoedInError(t *testing.T) {
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		_, _ = io.WriteString(w, `{"error":"bad request"}`)
	})
	svc, _ := newAdminTestServer(t, h)

	_, err := svc.ListKeys(context.Background())
	if err == nil {
		t.Fatal("expected error for 400, got nil")
	}
	if !strings.Contains(err.Error(), "400") || !strings.Contains(err.Error(), "bad request") {
		t.Errorf("error %v should contain 400 and the response body", err)
	}
}

func TestDoRequest_MalformedJSONReturnsDecodeError(t *testing.T) {
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = io.WriteString(w, "not-json-at-all")
	})
	svc, _ := newAdminTestServer(t, h)

	_, err := svc.ListKeys(context.Background())
	if err == nil {
		t.Fatal("expected decode error, got nil")
	}
	if !strings.Contains(err.Error(), "decode") {
		t.Errorf("error %v should mention decoding", err)
	}
}

func TestDoRequest_RetriesExhaustOnConnectionRefused(t *testing.T) {
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	addr := listener.Addr().String()
	if err := listener.Close(); err != nil {
		t.Fatalf("close listener: %v", err)
	}

	svc := NewGarageAdminService(&config.GarageConfig{
		AdminEndpoint: "http://" + addr,
		AdminToken:    "irrelevant",
	}, "")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	start := time.Now()
	_, err = svc.ListKeys(ctx)
	elapsed := time.Since(start)

	if err == nil {
		t.Fatal("expected error after retries exhaust, got nil")
	}
	if !strings.Contains(err.Error(), "max retries") {
		t.Errorf("expected max-retries error, got %v", err)
	}
	if elapsed < 200*time.Millisecond {
		t.Errorf("retries returned in %v — backoff loop not engaged", elapsed)
	}
}
