package storage

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

	"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
	"github.com/DeBrosOfficial/network/pkg/ipfs"
	"github.com/DeBrosOfficial/network/pkg/logging"
)

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

// mockIPFSClient implements the IPFSClient interface for testing.
type mockIPFSClient struct {
	addResp   *ipfs.AddResponse
	addErr    error
	pinResp   *ipfs.PinResponse
	pinErr    error
	pinStatus *ipfs.PinStatus
	pinStatErr error
	getReader io.ReadCloser
	getErr    error
	unpinErr  error
}

func (m *mockIPFSClient) Add(_ context.Context, _ io.Reader, _ string) (*ipfs.AddResponse, error) {
	return m.addResp, m.addErr
}

func (m *mockIPFSClient) Pin(_ context.Context, _ string, _ string, _ int) (*ipfs.PinResponse, error) {
	return m.pinResp, m.pinErr
}

func (m *mockIPFSClient) PinStatus(_ context.Context, _ string) (*ipfs.PinStatus, error) {
	return m.pinStatus, m.pinStatErr
}

func (m *mockIPFSClient) Get(_ context.Context, _ string, _ string) (io.ReadCloser, error) {
	return m.getReader, m.getErr
}

func (m *mockIPFSClient) Unpin(_ context.Context, _ string) error {
	return m.unpinErr
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

func newTestLogger() *logging.ColoredLogger {
	logger, _ := logging.NewColoredLogger(logging.ComponentStorage, false)
	return logger
}

func newTestHandlers(client IPFSClient) *Handlers {
	return New(client, newTestLogger(), Config{
		IPFSReplicationFactor: 3,
		IPFSAPIURL:            "http://localhost:5001",
	}, nil) // db=nil -> ownership checks bypassed
}

// withNamespace returns a request with the namespace context key set.
func withNamespace(r *http.Request, ns string) *http.Request {
	ctx := context.WithValue(r.Context(), ctxkeys.NamespaceOverride, ns)
	return r.WithContext(ctx)
}

// decodeBody decodes a JSON response body into a map.
func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
	t.Helper()
	var body map[string]interface{}
	if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	return body
}

// ---------------------------------------------------------------------------
// Tests: getNamespaceFromContext
// ---------------------------------------------------------------------------

func TestGetNamespaceFromContext_Present(t *testing.T) {
	h := newTestHandlers(nil)
	ctx := context.WithValue(context.Background(), ctxkeys.NamespaceOverride, "my-ns")

	got := h.getNamespaceFromContext(ctx)
	if got != "my-ns" {
		t.Errorf("expected 'my-ns', got %q", got)
	}
}

func TestGetNamespaceFromContext_Missing(t *testing.T) {
	h := newTestHandlers(nil)

	got := h.getNamespaceFromContext(context.Background())
	if got != "" {
		t.Errorf("expected empty string, got %q", got)
	}
}

func TestGetNamespaceFromContext_WrongType(t *testing.T) {
	h := newTestHandlers(nil)
	ctx := context.WithValue(context.Background(), ctxkeys.NamespaceOverride, 12345)

	got := h.getNamespaceFromContext(ctx)
	if got != "" {
		t.Errorf("expected empty string for wrong type, got %q", got)
	}
}

// ---------------------------------------------------------------------------
// Tests: UploadHandler
// ---------------------------------------------------------------------------

func TestUploadHandler_NilIPFS(t *testing.T) {
	h := newTestHandlers(nil) // nil IPFS client
	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("expected 503, got %d", rec.Code)
	}
}

func TestUploadHandler_InvalidMethod(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/upload", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestUploadHandler_MissingNamespace(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	// No namespace in context
	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"dGVzdA=="}`))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", rec.Code)
	}
}

func TestUploadHandler_InvalidJSON(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader("not json"))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
}

func TestUploadHandler_MissingData(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"name":"test.txt"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "data field required") {
		t.Errorf("expected 'data field required' error, got %q", errMsg)
	}
}

func TestUploadHandler_InvalidBase64(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"!!!invalid!!!"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "base64") {
		t.Errorf("expected base64 decode error, got %q", errMsg)
	}
}

func TestUploadHandler_PUTNotAllowed(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPut, "/v1/storage/upload", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestUploadHandler_Success(t *testing.T) {
	mock := &mockIPFSClient{
		addResp: &ipfs.AddResponse{
			Cid:  "QmTestCID1234567890123456789012345678901234",
			Name: "test.txt",
			Size: 4,
		},
		pinResp: &ipfs.PinResponse{
			Cid:  "QmTestCID1234567890123456789012345678901234",
			Name: "test.txt",
		},
	}
	h := newTestHandlers(mock)

	// "dGVzdA==" is base64("test")
	req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"dGVzdA==","name":"test.txt"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UploadHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
	}

	body := decodeBody(t, rec)
	if body["cid"] != "QmTestCID1234567890123456789012345678901234" {
		t.Errorf("unexpected cid: %v", body["cid"])
	}
}

// ---------------------------------------------------------------------------
// Tests: DownloadHandler
// ---------------------------------------------------------------------------

func TestDownloadHandler_NilIPFS(t *testing.T) {
	h := newTestHandlers(nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmSomeCID", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.DownloadHandler(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("expected 503, got %d", rec.Code)
	}
}

func TestDownloadHandler_InvalidMethod(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/get/QmSomeCID", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.DownloadHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestDownloadHandler_MissingCID(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.DownloadHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "cid required") {
		t.Errorf("expected 'cid required' error, got %q", errMsg)
	}
}

func TestDownloadHandler_MissingNamespace(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	// No namespace in context
	req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmSomeCID", nil)
	rec := httptest.NewRecorder()

	h.DownloadHandler(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", rec.Code)
	}
}

func TestDownloadHandler_Success(t *testing.T) {
	mock := &mockIPFSClient{
		getReader: io.NopCloser(strings.NewReader("file contents")),
	}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmTestCID", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.DownloadHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
	}
	if ct := rec.Header().Get("Content-Type"); ct != "application/octet-stream" {
		t.Errorf("expected application/octet-stream, got %q", ct)
	}
	if rec.Body.String() != "file contents" {
		t.Errorf("expected 'file contents', got %q", rec.Body.String())
	}
}

// ---------------------------------------------------------------------------
// Tests: StatusHandler
// ---------------------------------------------------------------------------

func TestStatusHandler_NilIPFS(t *testing.T) {
	h := newTestHandlers(nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/QmSomeCID", nil)
	rec := httptest.NewRecorder()

	h.StatusHandler(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("expected 503, got %d", rec.Code)
	}
}

func TestStatusHandler_InvalidMethod(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/status/QmSomeCID", nil)
	rec := httptest.NewRecorder()

	h.StatusHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestStatusHandler_MissingCID(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/", nil)
	rec := httptest.NewRecorder()

	h.StatusHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "cid required") {
		t.Errorf("expected 'cid required' error, got %q", errMsg)
	}
}

func TestStatusHandler_Success(t *testing.T) {
	mock := &mockIPFSClient{
		pinStatus: &ipfs.PinStatus{
			Cid:    "QmTestCID",
			Name:   "test.txt",
			Status: "pinned",
			Peers:  []string{"peer1", "peer2"},
		},
	}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/QmTestCID", nil)
	rec := httptest.NewRecorder()

	h.StatusHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	if body["cid"] != "QmTestCID" {
		t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
	}
	if body["status"] != "pinned" {
		t.Errorf("expected status='pinned', got %v", body["status"])
	}
}

// ---------------------------------------------------------------------------
// Tests: PinHandler
// ---------------------------------------------------------------------------

func TestPinHandler_NilIPFS(t *testing.T) {
	h := newTestHandlers(nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTest"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("expected 503, got %d", rec.Code)
	}
}

func TestPinHandler_InvalidMethod(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/pin", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestPinHandler_InvalidJSON(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader("bad json"))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
}

func TestPinHandler_MissingCID(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"name":"test"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "cid required") {
		t.Errorf("expected 'cid required' error, got %q", errMsg)
	}
}

func TestPinHandler_MissingNamespace(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	// No namespace in context
	req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTest"}`))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", rec.Code)
	}
}

func TestPinHandler_Success(t *testing.T) {
	mock := &mockIPFSClient{
		pinResp: &ipfs.PinResponse{
			Cid:  "QmTestCID",
			Name: "test.txt",
		},
	}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTestCID","name":"test.txt"}`))
	req.Header.Set("Content-Type", "application/json")
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.PinHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
	}
	body := decodeBody(t, rec)
	if body["cid"] != "QmTestCID" {
		t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
	}
}

// ---------------------------------------------------------------------------
// Tests: UnpinHandler
// ---------------------------------------------------------------------------

func TestUnpinHandler_NilIPFS(t *testing.T) {
	h := newTestHandlers(nil)
	req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTest", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Errorf("expected 503, got %d", rec.Code)
	}
}

func TestUnpinHandler_InvalidMethod(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodGet, "/v1/storage/unpin/QmTest", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestUnpinHandler_MissingCID(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected 400, got %d", rec.Code)
	}
	body := decodeBody(t, rec)
	errMsg, _ := body["error"].(string)
	if !strings.Contains(errMsg, "cid required") {
		t.Errorf("expected 'cid required' error, got %q", errMsg)
	}
}

func TestUnpinHandler_MissingNamespace(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	// No namespace in context
	req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTest", nil)
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Errorf("expected 401, got %d", rec.Code)
	}
}

func TestUnpinHandler_POSTNotAllowed(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodPost, "/v1/storage/unpin/QmTest", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405, got %d", rec.Code)
	}
}

func TestUnpinHandler_Success(t *testing.T) {
	mock := &mockIPFSClient{}
	h := newTestHandlers(mock)

	req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTestCID", nil)
	req = withNamespace(req, "test-ns")
	rec := httptest.NewRecorder()

	h.UnpinHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
	}
	body := decodeBody(t, rec)
	if body["status"] != "ok" {
		t.Errorf("expected status='ok', got %v", body["status"])
	}
	if body["cid"] != "QmTestCID" {
		t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
	}
}

// ---------------------------------------------------------------------------
// Tests: base64Decode helper
// ---------------------------------------------------------------------------

func TestBase64Decode_Valid(t *testing.T) {
	// "dGVzdA==" is base64("test")
	data, err := base64Decode("dGVzdA==")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if string(data) != "test" {
		t.Errorf("expected 'test', got %q", string(data))
	}
}

func TestBase64Decode_Invalid(t *testing.T) {
	_, err := base64Decode("!!!not-valid-base64!!!")
	if err == nil {
		t.Error("expected error for invalid base64, got nil")
	}
}

func TestBase64Decode_Empty(t *testing.T) {
	data, err := base64Decode("")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(data) != 0 {
		t.Errorf("expected empty slice, got %d bytes", len(data))
	}
}

// ---------------------------------------------------------------------------
// Tests: recordCIDOwnership / checkCIDOwnership / updatePinStatus with nil DB
// ---------------------------------------------------------------------------

func TestRecordCIDOwnership_NilDB(t *testing.T) {
	h := newTestHandlers(&mockIPFSClient{})
	err := h.recordCIDOwnership(context.Background(), "cid", "ns", "name", "uploader", 100)
	if err != nil {
		t.Errorf("expected nil error with nil db, got %v", err)
	}
}

func TestCheckCIDOwnership_NilDB(t *testing.T) {
	h := newTestHandlers(&mockIPFSClient{})
	hasAccess, err := h.checkCIDOwnership(context.Background(), "cid", "ns")
	if err != nil {
		t.Errorf("expected nil error with nil db, got %v", err)
	}
	if !hasAccess {
		t.Error("expected true (allow access) when db is nil")
	}
}

func TestUpdatePinStatus_NilDB(t *testing.T) {
	h := newTestHandlers(&mockIPFSClient{})
	err := h.updatePinStatus(context.Background(), "cid", "ns", true)
	if err != nil {
		t.Errorf("expected nil error with nil db, got %v", err)
	}
}
