package handlers_test

import (
	"bullet-cloud-api/internal/auth"
	"bullet-cloud-api/internal/models"
	"bullet-cloud-api/internal/products"
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/mux"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// Define a consistent JWT secret for testing
const testJwtSecret = "um-segredo-super-secreto-para-testes-123"

// --- Mock Repositories (Generated by mockery or similar) ---

// MockUserRepository is a mock implementation of UserRepository
type MockUserRepository struct {
	mock.Mock
}

func (m *MockUserRepository) Create(ctx context.Context, name, email, passwordHash string) (*models.User, error) {
	args := m.Called(ctx, name, email, passwordHash)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
	args := m.Called(ctx, email)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserRepository) FindByID(ctx context.Context, id uuid.UUID) (*models.User, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}

// MockProductRepository is a mock implementation of ProductRepository
type MockProductRepository struct {
	mock.Mock
}

func (m *MockProductRepository) FindAll(ctx context.Context) ([]models.Product, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Product), args.Error(1)
}
func (m *MockProductRepository) FindByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Product), args.Error(1)
}
func (m *MockProductRepository) Create(ctx context.Context, product *models.Product) (*models.Product, error) {
	args := m.Called(ctx, product)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Product), args.Error(1)
}
func (m *MockProductRepository) Update(ctx context.Context, id uuid.UUID, product *models.Product) (*models.Product, error) {
	args := m.Called(ctx, id, product)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Product), args.Error(1)
}
func (m *MockProductRepository) Delete(ctx context.Context, id uuid.UUID) error {
	args := m.Called(ctx, id)
	return args.Error(0)
}
func (m *MockProductRepository) Search(ctx context.Context, query string) ([]models.Product, error) {
	args := m.Called(ctx, query)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Product), args.Error(1)
}
func (m *MockProductRepository) FindByCategoryID(ctx context.Context, categoryID uuid.UUID) ([]models.Product, error) {
	args := m.Called(ctx, categoryID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Product), args.Error(1)
}

// MockCategoryRepository is a mock implementation of CategoryRepository
type MockCategoryRepository struct {
	mock.Mock
}

func (m *MockCategoryRepository) FindAll(ctx context.Context) ([]models.Category, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Category), args.Error(1)
}
func (m *MockCategoryRepository) FindByID(ctx context.Context, id uuid.UUID) (*models.Category, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Category), args.Error(1)
}
func (m *MockCategoryRepository) Create(ctx context.Context, category *models.Category) error {
	args := m.Called(ctx, category)
	return args.Error(0)
}
func (m *MockCategoryRepository) Update(ctx context.Context, category *models.Category) error {
	args := m.Called(ctx, category)
	return args.Error(0)
}
func (m *MockCategoryRepository) Delete(ctx context.Context, id uuid.UUID) error {
	args := m.Called(ctx, id)
	return args.Error(0)
}

// MockAddressRepository is a mock implementation of AddressRepository
type MockAddressRepository struct {
	mock.Mock
}

func (m *MockAddressRepository) Create(ctx context.Context, address *models.Address) (*models.Address, error) {
	args := m.Called(ctx, address)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Address), args.Error(1)
}
func (m *MockAddressRepository) FindByUserID(ctx context.Context, userID uuid.UUID) ([]models.Address, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Address), args.Error(1)
}
func (m *MockAddressRepository) FindByUserAndID(ctx context.Context, userID, addressID uuid.UUID) (*models.Address, error) {
	args := m.Called(ctx, userID, addressID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Address), args.Error(1)
}
func (m *MockAddressRepository) Update(ctx context.Context, userID, addressID uuid.UUID, address *models.Address) (*models.Address, error) {
	args := m.Called(ctx, userID, addressID, address)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Address), args.Error(1)
}
func (m *MockAddressRepository) Delete(ctx context.Context, userID, addressID uuid.UUID) error {
	args := m.Called(ctx, userID, addressID)
	return args.Error(0)
}
func (m *MockAddressRepository) SetDefault(ctx context.Context, userID, addressID uuid.UUID) error {
	args := m.Called(ctx, userID, addressID)
	return args.Error(0)
}

// MockCartRepository is a mock implementation of CartRepository
type MockCartRepository struct {
	mock.Mock
}

func (m *MockCartRepository) GetOrCreateCartByUserID(ctx context.Context, userID uuid.UUID) (*models.Cart, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Cart), args.Error(1)
}
func (m *MockCartRepository) GetCartItems(ctx context.Context, cartID uuid.UUID) ([]models.CartItem, error) {
	args := m.Called(ctx, cartID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.CartItem), args.Error(1)
}
func (m *MockCartRepository) AddItem(ctx context.Context, cartID, productID uuid.UUID, quantity int, price float64) (*models.CartItem, error) {
	args := m.Called(ctx, cartID, productID, quantity, price)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.CartItem), args.Error(1)
}
func (m *MockCartRepository) UpdateItemQuantity(ctx context.Context, cartID, productID uuid.UUID, quantity int) (*models.CartItem, error) {
	args := m.Called(ctx, cartID, productID, quantity)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.CartItem), args.Error(1)
}
func (m *MockCartRepository) RemoveItem(ctx context.Context, cartID, productID uuid.UUID) error {
	args := m.Called(ctx, cartID, productID)
	return args.Error(0)
}
func (m *MockCartRepository) FindCartItem(ctx context.Context, cartID, productID uuid.UUID) (*models.CartItem, error) {
	args := m.Called(ctx, cartID, productID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.CartItem), args.Error(1)
}
func (m *MockCartRepository) ClearCart(ctx context.Context, cartID uuid.UUID) error {
	args := m.Called(ctx, cartID)
	return args.Error(0)
}

// MockPasswordHasher is a mock implementation of PasswordHasher
type MockPasswordHasher struct {
	mock.Mock
}

func (m *MockPasswordHasher) HashPassword(password string) (string, error) {
	args := m.Called(password)
	return args.String(0), args.Error(1)
}

// CheckPassword mocks the CheckPassword method from the PasswordHasher interface.
func (m *MockPasswordHasher) CheckPassword(hashedPassword, password string) error {
	args := m.Called(hashedPassword, password)
	// Return the error stored in the mock expectations (index 0)
	return args.Error(0)
}

// --- Test Helpers ---

// setupBaseTest initializes common components for handler tests.
func setupBaseTest(t *testing.T) (context.Context, *httptest.ResponseRecorder, *mux.Router, *MockUserRepository, *MockProductRepository, *MockCategoryRepository, *MockAddressRepository, *MockCartRepository, string) {
	t.Helper()
	ctx := context.Background()
	rr := httptest.NewRecorder()
	router := mux.NewRouter()

	// Mocks
	mockUserRepo := new(MockUserRepository)
	mockProductRepo := new(MockProductRepository)
	mockCategoryRepo := new(MockCategoryRepository)
	mockAddressRepo := new(MockAddressRepository)
	mockCartRepo := new(MockCartRepository)

	// Generate a valid test token using the test secret
	testUserID := uuid.New()
	testToken, err := auth.GenerateToken(testUserID, testJwtSecret, time.Hour*1) // Use test constant
	require.NoError(t, err, "Failed to generate test token")

	// Mock FindByID for authentication middleware success
	mockUserRepo.On("FindByID", mock.Anything, testUserID).Return(&models.User{ID: testUserID}, nil).Maybe()

	return ctx, rr, router, mockUserRepo, mockProductRepo, mockCategoryRepo, mockAddressRepo, mockCartRepo, testToken
}

// Helper function to generate a test token for a specific user ID
func generateTestToken(userID uuid.UUID) (string, error) {
	// Use test constant for secret and a default expiry
	return auth.GenerateToken(userID, testJwtSecret, time.Hour*1)
}

// executeRequestAndAssert executes an HTTP request and asserts the expected status code and body.
func executeRequestAndAssert(t *testing.T, router *mux.Router, req *http.Request, expectedStatus int, expectedBody string) *httptest.ResponseRecorder {
	t.Helper()
	rr := httptest.NewRecorder()
	router.ServeHTTP(rr, req)

	require.Equal(t, expectedStatus, rr.Code, "handler returned wrong status code")

	if expectedBody != "" {
		// Trim whitespace for comparison, useful for JSON responses
		actualBody := strings.TrimSpace(rr.Body.String())
		expectedBodyTrimmed := strings.TrimSpace(expectedBody)
		// Use JSONEq for better JSON comparison if applicable, otherwise use Contains
		if strings.HasPrefix(expectedBodyTrimmed, "{") || strings.HasPrefix(expectedBodyTrimmed, "[") {
			require.JSONEq(t, expectedBodyTrimmed, actualBody, "handler returned unexpected body")
		} else {
			require.Contains(t, actualBody, expectedBodyTrimmed, "handler returned unexpected body content")
		}
	}

	return rr
}

// --- Dummy Config and DB Pool (if needed for specific tests) ---
func setupDummyDbPool(t *testing.T) *pgxpool.Pool {
	// This is tricky without a real DB or testcontainer.
	// For most handler tests relying on mocks, this might not be needed.
	// If direct DB interaction is tested, consider testcontainers.
	t.Skip("Skipping test requiring DB pool setup")
	return nil
}

// --- Mock Models (Already in internal/models) ---
// Use models directly: models.User, models.Product, etc.
// Create instances as needed for test data.
// Example:
// testProduct := models.Product{ID: uuid.New(), Name: "Test Gadget", ...}

// --- Mock Setup Helpers ---

// mockReturn is a helper to reduce duplication in mock return value handling
func mockReturn[T any](args mock.Arguments, index int) T {
	if args.Get(index) == nil {
		var zero T
		return zero
	}
	return args.Get(index).(T)
}

// Mocks successful GetOrCreateCartByUserID call
func mockGetOrCreateCartSuccess(m *MockCartRepository, userID uuid.UUID, cartToReturn *models.Cart) {
	m.On("GetOrCreateCartByUserID", mock.Anything, userID).Return(cartToReturn, nil).Once()
}

// Mocks failed GetOrCreateCartByUserID call
func mockGetOrCreateCartError(m *MockCartRepository, userID uuid.UUID) {
	m.On("GetOrCreateCartByUserID", mock.Anything, userID).Return(nil, assert.AnError).Once()
}

// Mocks successful GetCartItems call
func mockGetCartItemsSuccess(m *MockCartRepository, cartID uuid.UUID, itemsToReturn []models.CartItem) {
	m.On("GetCartItems", mock.Anything, cartID).Return(itemsToReturn, nil).Once()
}

// Mocks failed GetCartItems call
func mockGetCartItemsError(m *MockCartRepository, cartID uuid.UUID) {
	m.On("GetCartItems", mock.Anything, cartID).Return(nil, assert.AnError).Once()
}

// Mocks successful FindProductByID call
func mockFindProductSuccess(m *MockProductRepository, productToReturn *models.Product) {
	m.On("FindByID", mock.Anything, productToReturn.ID).Return(productToReturn, nil).Once()
}

// Mocks FindProductByID call returning Not Found
func mockFindProductNotFound(m *MockProductRepository, productID uuid.UUID) {
	m.On("FindByID", mock.Anything, productID).Return(nil, products.ErrProductNotFound).Once()
}

// Mocks failed FindProductByID call
func mockFindProductError(m *MockProductRepository, productID uuid.UUID) {
	m.On("FindByID", mock.Anything, productID).Return(nil, assert.AnError).Once()
}

// Mocks successful AddItem call
func mockAddItemSuccess(m *MockCartRepository, cartID, productID uuid.UUID, quantity int, price float64, itemToReturn *models.CartItem) {
	m.On("AddItem", mock.Anything, cartID, productID, quantity, price).Return(itemToReturn, nil).Once()
}

// Mocks failed AddItem call
func mockAddItemError(m *MockCartRepository, cartID, productID uuid.UUID, quantity int, price float64) {
	m.On("AddItem", mock.Anything, cartID, productID, quantity, price).Return(nil, assert.AnError).Once()
}

// Mocks successful RemoveItem call
func mockRemoveItemSuccess(m *MockCartRepository, cartID, productID uuid.UUID) {
	m.On("RemoveItem", mock.Anything, cartID, productID).Return(nil).Once()
}

// Mocks RemoveItem call returning Not Found (or similar logical error)
func mockRemoveItemNotFound(m *MockCartRepository, cartID, productID uuid.UUID) {
	m.On("RemoveItem", mock.Anything, cartID, productID).Return(assert.AnError).Once()
}

// Mocks failed RemoveItem call
func mockRemoveItemError(m *MockCartRepository, cartID, productID uuid.UUID) {
	m.On("RemoveItem", mock.Anything, cartID, productID).Return(assert.AnError).Once()
}

// Mocks successful UpdateItemQuantity call
func mockUpdateItemQuantitySuccess(m *MockCartRepository, cartID, productID uuid.UUID, quantity int, itemToReturn *models.CartItem) {
	m.On("UpdateItemQuantity", mock.Anything, cartID, productID, quantity).Return(itemToReturn, nil).Once()
}

// Mocks UpdateItemQuantity call returning Not Found
func mockUpdateItemQuantityNotFound(m *MockCartRepository, cartID, productID uuid.UUID, quantity int) {
	m.On("UpdateItemQuantity", mock.Anything, cartID, productID, quantity).Return(nil, assert.AnError).Once()
}

// Mocks failed UpdateItemQuantity call
func mockUpdateItemQuantityError(m *MockCartRepository, cartID, productID uuid.UUID, quantity int) {
	m.On("UpdateItemQuantity", mock.Anything, cartID, productID, quantity).Return(nil, assert.AnError).Once()
}

// Mocks successful ClearCart call
func mockClearCartSuccess(m *MockCartRepository, cartID uuid.UUID) {
	m.On("ClearCart", mock.Anything, cartID).Return(nil).Once()
}

// Mocks failed ClearCart call
func mockClearCartError(m *MockCartRepository, cartID uuid.UUID) {
	m.On("ClearCart", mock.Anything, cartID).Return(assert.AnError).Once()
}
