package handlers

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

	"Noooste/garage-ui/internal/models"
	"Noooste/garage-ui/internal/services/mocks"

	"github.com/gofiber/fiber/v3"
)

func newBucketsTestApp(t *testing.T) (*fiber.App, *mocks.AdminMock) {
	t.Helper()
	admin := &mocks.AdminMock{}
	h := NewBucketHandler(admin, nil) // s3 unused in this handler
	app := fiber.New()
	app.Get("/buckets", h.ListBuckets)
	app.Post("/buckets", h.CreateBucket)
	app.Get("/buckets/:name", h.GetBucketInfo)
	app.Delete("/buckets/:name", h.DeleteBucket)
	app.Post("/buckets/:name/permissions", h.GrantBucketPermission)
	app.Put("/buckets/:name/website", h.UpdateBucketWebsite)
	return app, admin
}

func decodeJSON(t *testing.T, r io.Reader, v any) {
	t.Helper()
	if err := json.NewDecoder(r).Decode(v); err != nil {
		t.Fatalf("decode: %v", err)
	}
}

// --- ListBuckets ---

func TestListBuckets_MapsAliasesAndStats(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) {
		return []models.ListBucketsResponseItem{
			{ID: "id-1", Created: time.Unix(0, 0), GlobalAliases: []string{"alpha"}},
			{ID: "id-2", Created: time.Unix(0, 0), GlobalAliases: []string{}},  // skipped: no global alias
			{ID: "id-3", Created: time.Unix(0, 0), GlobalAliases: []string{"gamma"}},
		}, nil
	}
	admin.GetBucketInfoByAliasFn = func(_ context.Context, alias string) (*models.GarageBucketInfo, error) {
		switch alias {
		case "alpha":
			return &models.GarageBucketInfo{ID: "id-1", Objects: 10, Bytes: 100, WebsiteAccess: true}, nil
		case "gamma":
			return nil, errors.New("detail fetch failed") // degraded path
		}
		return nil, errors.New("unexpected alias: " + alias)
	}
	req := httptest.NewRequest(http.MethodGet, "/buckets", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
	var body struct {
		Data models.BucketListResponse `json:"data"`
	}
	decodeJSON(t, resp.Body, &body)
	if body.Data.Count != 2 {
		t.Errorf("count = %d, want 2 (id-2 skipped)", body.Data.Count)
	}
	// alpha has stats; gamma degraded to no stats (ObjectCount/Size nil).
	var foundAlpha, foundGamma bool
	for _, b := range body.Data.Buckets {
		if b.Name == "alpha" {
			foundAlpha = true
			if b.ObjectCount == nil || *b.ObjectCount != 10 {
				t.Errorf("alpha.ObjectCount = %v, want 10", b.ObjectCount)
			}
			if !b.WebsiteAccess {
				t.Error("alpha.WebsiteAccess false")
			}
		}
		if b.Name == "gamma" {
			foundGamma = true
			if b.ObjectCount != nil {
				t.Errorf("gamma.ObjectCount = %v, want nil (degraded)", *b.ObjectCount)
			}
		}
	}
	if !foundAlpha || !foundGamma {
		t.Errorf("missing buckets: alpha=%v gamma=%v", foundAlpha, foundGamma)
	}
}

func TestListBuckets_AdminErrorReturns500(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) {
		return nil, errors.New("boom")
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", resp.StatusCode)
	}
}

// --- CreateBucket ---

func TestCreateBucket_Success201(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.CreateBucketFn = func(_ context.Context, r models.CreateBucketAdminRequest) (*models.GarageBucketInfo, error) {
		if r.GlobalAlias == nil || *r.GlobalAlias != "new-bucket" {
			t.Errorf("GlobalAlias = %v, want 'new-bucket'", r.GlobalAlias)
		}
		return &models.GarageBucketInfo{ID: "id-new"}, nil
	}
	body, _ := json.Marshal(map[string]string{"name": "new-bucket"})
	req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("status = %d, want 201", resp.StatusCode)
	}
}

func TestCreateBucket_MissingNameReturns400(t *testing.T) {
	app, _ := newBucketsTestApp(t)
	body, _ := json.Marshal(map[string]string{})
	req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

func TestCreateBucket_MalformedJSONReturns400(t *testing.T) {
	app, _ := newBucketsTestApp(t)
	req := httptest.NewRequest(http.MethodPost, "/buckets", strings.NewReader("{not-json"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

func TestCreateBucket_AdminErrorReturns500(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.CreateBucketFn = func(_ context.Context, _ models.CreateBucketAdminRequest) (*models.GarageBucketInfo, error) {
		return nil, errors.New("boom")
	}
	body, _ := json.Marshal(map[string]string{"name": "x"})
	req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", resp.StatusCode)
	}
}

// --- GetBucketInfo ---

func TestGetBucketInfo_Success(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, alias string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1", Bytes: 1, Objects: 1}, nil
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/alpha", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
}

func TestGetBucketInfo_NotFound404(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return nil, nil // nil pointer, nil error → 404
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/missing", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("status = %d, want 404", resp.StatusCode)
	}
}

func TestGetBucketInfo_ServiceErrorReturns500(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return nil, errors.New("boom")
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/alpha", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", resp.StatusCode)
	}
}

// --- DeleteBucket ---

func TestDeleteBucket_Success(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	admin.DeleteBucketFn = func(_ context.Context, id string) error {
		if id != "id-1" {
			t.Errorf("DeleteBucket id = %q, want id-1", id)
		}
		return nil
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/alpha", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
}

func TestDeleteBucket_NotFound(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return nil, nil
	}
	resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/missing", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("status = %d, want 404", resp.StatusCode)
	}
}

func TestDeleteBucket_AdminDeleteErrorReturns500(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	admin.DeleteBucketFn = func(_ context.Context, _ string) error { return errors.New("boom") }
	resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/alpha", nil))
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", resp.StatusCode)
	}
}

// --- GrantBucketPermission ---

func TestGrantBucketPermission_Success(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	admin.AllowBucketKeyFn = func(_ context.Context, req models.BucketKeyPermRequest) (*models.GarageBucketInfo, error) {
		if req.BucketID != "id-1" || req.AccessKeyID != "AKIA" {
			t.Errorf("req = %+v", req)
		}
		if !req.Permissions.Read || !req.Permissions.Write || req.Permissions.Owner {
			t.Errorf("perms = %+v", req.Permissions)
		}
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	body, _ := json.Marshal(models.GrantBucketPermissionRequest{
		AccessKeyID: "AKIA",
		Permissions: models.BucketKeyPermission{Read: true, Write: true},
	})
	req := httptest.NewRequest(http.MethodPost, "/buckets/alpha/permissions", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
}

func TestGrantBucketPermission_MissingAccessKey400(t *testing.T) {
	app, _ := newBucketsTestApp(t)
	body, _ := json.Marshal(map[string]any{"accessKeyId": "", "permissions": map[string]bool{"read": true}})
	req := httptest.NewRequest(http.MethodPost, "/buckets/alpha/permissions", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

func TestGrantBucketPermission_BucketNotFound404(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return nil, nil
	}
	body, _ := json.Marshal(models.GrantBucketPermissionRequest{AccessKeyID: "AKIA"})
	req := httptest.NewRequest(http.MethodPost, "/buckets/missing/permissions", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("status = %d, want 404", resp.StatusCode)
	}
}

// --- UpdateBucketWebsite ---

func TestUpdateBucketWebsite_EnableWithIndexDocument(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	admin.UpdateBucketFn = func(_ context.Context, id string, req models.UpdateBucketRequest) (*models.GarageBucketInfo, error) {
		if req.WebsiteAccess == nil || !req.WebsiteAccess.Enabled {
			t.Errorf("WebsiteAccess = %+v", req.WebsiteAccess)
		}
		if req.WebsiteAccess.IndexDocument == nil || *req.WebsiteAccess.IndexDocument != "index.html" {
			t.Errorf("IndexDocument = %v", req.WebsiteAccess.IndexDocument)
		}
		return &models.GarageBucketInfo{ID: id, WebsiteAccess: true}, nil
	}
	body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: true, IndexDocument: "index.html"})
	req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
}

func TestUpdateBucketWebsite_EnableWithoutIndexDocumentReturns400(t *testing.T) {
	app, _ := newBucketsTestApp(t)
	body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: true})
	req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

func TestUpdateBucketWebsite_Disable(t *testing.T) {
	app, admin := newBucketsTestApp(t)
	admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	admin.UpdateBucketFn = func(_ context.Context, _ string, req models.UpdateBucketRequest) (*models.GarageBucketInfo, error) {
		if req.WebsiteAccess == nil || req.WebsiteAccess.Enabled {
			t.Errorf("expected Enabled=false, got %+v", req.WebsiteAccess)
		}
		return &models.GarageBucketInfo{ID: "id-1"}, nil
	}
	body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: false})
	req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d", resp.StatusCode)
	}
}
