package testutil

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"github.com/treytartt/honeydue-api/internal/apperrors"
	"github.com/treytartt/honeydue-api/internal/i18n"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/validator"
)

var i18nOnce sync.Once

// SetupTestDB creates an in-memory SQLite database for testing
func SetupTestDB(t *testing.T) *gorm.DB {
	// Initialize i18n once for all tests
	i18nOnce.Do(func() {
		i18n.Init()
	})

	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	require.NoError(t, err)

	// Migrate all models
	err = db.AutoMigrate(
		&models.User{},
		&models.UserProfile{},
		&models.AuthToken{},
		&models.ConfirmationCode{},
		&models.PasswordResetCode{},
		&models.AdminUser{},
		&models.Residence{},
		&models.ResidenceType{},
		&models.ResidenceShareCode{},
		&models.Task{},
		&models.TaskCategory{},
		&models.TaskPriority{},
		&models.TaskFrequency{},
		&models.TaskCompletion{},
		&models.TaskCompletionImage{},
		&models.Contractor{},
		&models.ContractorSpecialty{},
		&models.Document{},
		&models.DocumentImage{},
		&models.Notification{},
		&models.NotificationPreference{},
		&models.APNSDevice{},
		&models.GCMDevice{},
		&models.AppleSocialAuth{},
		&models.GoogleSocialAuth{},
		&models.TaskReminderLog{},
		&models.UserSubscription{},
		&models.SubscriptionSettings{},
		&models.TierLimits{},
		&models.FeatureBenefit{},
		&models.UpgradeTrigger{},
		&models.Promotion{},
		&models.AuditLog{},
		&models.TaskTemplate{},
		&models.ClimateRegion{},
	)
	require.NoError(t, err)

	return db
}

// SetupTestRouter creates a test Echo router with the custom error handler.
// Uses apperrors.HTTPErrorHandler which is the base error handler shared with
// production (router.customHTTPErrorHandler). Both handle AppError, ValidationErrors,
// and echo.HTTPError identically. Production additionally maps legacy service sentinel
// errors (e.g., services.ErrTaskNotFound) which are being migrated to AppError types.
// Tests exercise handlers that return AppError, so this handler covers all test scenarios.
func SetupTestRouter() *echo.Echo {
	e := echo.New()
	e.Validator = validator.NewCustomValidator()
	e.HTTPErrorHandler = apperrors.HTTPErrorHandler
	return e
}

// MakeRequest makes a test HTTP request and returns the response.
// Errors from JSON marshaling and HTTP request construction are checked and
// will panic if they occur, since these indicate programming errors in tests.
// Prefer MakeRequestT for new tests, which uses t.Fatal for better reporting.
func MakeRequest(router *echo.Echo, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
	var reqBody *bytes.Buffer
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			panic(fmt.Sprintf("testutil.MakeRequest: failed to marshal request body: %v", err))
		}
		reqBody = bytes.NewBuffer(jsonBody)
	} else {
		reqBody = bytes.NewBuffer(nil)
	}

	req, err := http.NewRequest(method, path, reqBody)
	if err != nil {
		panic(fmt.Sprintf("testutil.MakeRequest: failed to create HTTP request: %v", err))
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Token "+token)
	}

	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

// MakeRequestT is like MakeRequest but accepts a *testing.T for proper test
// failure reporting. Prefer this over MakeRequest in new tests.
func MakeRequestT(t *testing.T, router *echo.Echo, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
	t.Helper()

	var reqBody *bytes.Buffer
	if body != nil {
		jsonBody, err := json.Marshal(body)
		require.NoError(t, err, "failed to marshal request body")
		reqBody = bytes.NewBuffer(jsonBody)
	} else {
		reqBody = bytes.NewBuffer(nil)
	}

	req, err := http.NewRequest(method, path, reqBody)
	require.NoError(t, err, "failed to create HTTP request")
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Token "+token)
	}

	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

// ParseJSON parses JSON response body into a map
func ParseJSON(t *testing.T, body []byte) map[string]interface{} {
	var result map[string]interface{}
	err := json.Unmarshal(body, &result)
	require.NoError(t, err)
	return result
}

// ParseJSONArray parses JSON response body into an array
func ParseJSONArray(t *testing.T, body []byte) []map[string]interface{} {
	var result []map[string]interface{}
	err := json.Unmarshal(body, &result)
	require.NoError(t, err)
	return result
}

// CreateTestUser creates a test user in the database
func CreateTestUser(t *testing.T, db *gorm.DB, username, email, password string) *models.User {
	user := &models.User{
		Username: username,
		Email:    email,
		IsActive: true,
	}
	err := user.SetPassword(password)
	require.NoError(t, err)

	err = db.Create(user).Error
	require.NoError(t, err)

	return user
}

// CreateTestToken creates an auth token for a user
func CreateTestToken(t *testing.T, db *gorm.DB, userID uint) *models.AuthToken {
	token, err := models.GetOrCreateToken(db, userID)
	require.NoError(t, err)
	return token
}

// CreateTestResidenceType creates a test residence type
func CreateTestResidenceType(t *testing.T, db *gorm.DB, name string) *models.ResidenceType {
	rt := &models.ResidenceType{Name: name}
	err := db.Create(rt).Error
	require.NoError(t, err)
	return rt
}

// CreateTestResidence creates a test residence
func CreateTestResidence(t *testing.T, db *gorm.DB, ownerID uint, name string) *models.Residence {
	residence := &models.Residence{
		OwnerID:       ownerID,
		Name:          name,
		StreetAddress: "123 Test St",
		City:          "Test City",
		StateProvince: "TS",
		PostalCode:    "12345",
		Country:       "USA",
		IsActive:      true,
		IsPrimary:     true,
	}
	err := db.Create(residence).Error
	require.NoError(t, err)
	return residence
}

// CreateTestTaskCategory creates a test task category
func CreateTestTaskCategory(t *testing.T, db *gorm.DB, name string) *models.TaskCategory {
	cat := &models.TaskCategory{
		Name:         name,
		DisplayOrder: 1,
	}
	err := db.Create(cat).Error
	require.NoError(t, err)
	return cat
}

// CreateTestTaskPriority creates a test task priority
func CreateTestTaskPriority(t *testing.T, db *gorm.DB, name string, level int) *models.TaskPriority {
	priority := &models.TaskPriority{
		Name:         name,
		Level:        level,
		DisplayOrder: level,
	}
	err := db.Create(priority).Error
	require.NoError(t, err)
	return priority
}

// CreateTestTaskFrequency creates a test task frequency
func CreateTestTaskFrequency(t *testing.T, db *gorm.DB, name string, days *int) *models.TaskFrequency {
	freq := &models.TaskFrequency{
		Name:         name,
		Days:         days,
		DisplayOrder: 1,
	}
	err := db.Create(freq).Error
	require.NoError(t, err)
	return freq
}

// CreateTestTask creates a test task
func CreateTestTask(t *testing.T, db *gorm.DB, residenceID, createdByID uint, title string) *models.Task {
	task := &models.Task{
		ResidenceID: residenceID,
		CreatedByID: createdByID,
		Title:       title,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)
	return task
}

// SeedLookupData seeds all lookup tables with test data.
// All GORM create operations are checked for errors to prevent silent failures
// that could cause misleading test results.
func SeedLookupData(t *testing.T, db *gorm.DB) {
	t.Helper()

	// Residence types
	residenceTypes := []models.ResidenceType{
		{Name: "House"},
		{Name: "Apartment"},
		{Name: "Condo"},
		{Name: "Townhouse"},
	}
	for i := range residenceTypes {
		err := db.Create(&residenceTypes[i]).Error
		require.NoError(t, err, "failed to seed residence type: %s", residenceTypes[i].Name)
	}

	// Task categories
	categories := []models.TaskCategory{
		{Name: "Plumbing", DisplayOrder: 1},
		{Name: "Electrical", DisplayOrder: 2},
		{Name: "HVAC", DisplayOrder: 3},
		{Name: "General", DisplayOrder: 99},
	}
	for i := range categories {
		err := db.Create(&categories[i]).Error
		require.NoError(t, err, "failed to seed task category: %s", categories[i].Name)
	}

	// Task priorities
	priorities := []models.TaskPriority{
		{Name: "Low", Level: 1, DisplayOrder: 1},
		{Name: "Medium", Level: 2, DisplayOrder: 2},
		{Name: "High", Level: 3, DisplayOrder: 3},
		{Name: "Urgent", Level: 4, DisplayOrder: 4},
	}
	for i := range priorities {
		err := db.Create(&priorities[i]).Error
		require.NoError(t, err, "failed to seed task priority: %s", priorities[i].Name)
	}

	// Task frequencies
	days7 := 7
	days30 := 30
	frequencies := []models.TaskFrequency{
		{Name: "Once", Days: nil, DisplayOrder: 1},
		{Name: "Weekly", Days: &days7, DisplayOrder: 2},
		{Name: "Monthly", Days: &days30, DisplayOrder: 3},
	}
	for i := range frequencies {
		err := db.Create(&frequencies[i]).Error
		require.NoError(t, err, "failed to seed task frequency: %s", frequencies[i].Name)
	}

	// Contractor specialties
	specialties := []models.ContractorSpecialty{
		{Name: "Plumber"},
		{Name: "Electrician"},
		{Name: "HVAC Technician"},
		{Name: "Handyman"},
	}
	for i := range specialties {
		err := db.Create(&specialties[i]).Error
		require.NoError(t, err, "failed to seed contractor specialty: %s", specialties[i].Name)
	}
}

// AssertJSONField asserts that a JSON field has the expected value
func AssertJSONField(t *testing.T, data map[string]interface{}, field string, expected interface{}) {
	actual, ok := data[field]
	require.True(t, ok, "field %s not found in response", field)
	require.Equal(t, expected, actual, "field %s has unexpected value", field)
}

// AssertJSONFieldExists asserts that a JSON field exists
func AssertJSONFieldExists(t *testing.T, data map[string]interface{}, field string) {
	_, ok := data[field]
	require.True(t, ok, "field %s not found in response", field)
}

// AssertStatusCode asserts the HTTP status code
func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, expected int) {
	require.Equal(t, expected, w.Code, "unexpected status code: %s", w.Body.String())
}

// MockAuthMiddleware creates middleware that sets a test user in context
func MockAuthMiddleware(user *models.User) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			c.Set("auth_user", user)
			c.Set("auth_token", "test-token")
			return next(c)
		}
	}
}

// CreateTestContractor creates a test contractor
func CreateTestContractor(t *testing.T, db *gorm.DB, residenceID, createdByID uint, name string) *models.Contractor {
	contractor := &models.Contractor{
		ResidenceID: &residenceID,
		CreatedByID: createdByID,
		Name:        name,
		IsActive:    true,
	}
	err := db.Create(contractor).Error
	require.NoError(t, err)
	return contractor
}

// CreateTestContractorSpecialty creates a test contractor specialty
func CreateTestContractorSpecialty(t *testing.T, db *gorm.DB, name string) *models.ContractorSpecialty {
	specialty := &models.ContractorSpecialty{Name: name}
	err := db.Create(specialty).Error
	require.NoError(t, err)
	return specialty
}

// CreateTestDocument creates a test document
func CreateTestDocument(t *testing.T, db *gorm.DB, residenceID, createdByID uint, title string) *models.Document {
	doc := &models.Document{
		ResidenceID:  residenceID,
		CreatedByID:  createdByID,
		Title:        title,
		DocumentType: "general",
		FileURL:      "https://example.com/doc.pdf",
	}
	err := db.Create(doc).Error
	require.NoError(t, err)
	return doc
}

// AssertAppError asserts that an error is an AppError with a specific status code and message key
func AssertAppError(t *testing.T, err error, expectedCode int, expectedMessageKey string) {
	require.Error(t, err, "expected an error")

	var appErr *apperrors.AppError
	require.ErrorAs(t, err, &appErr, "expected an AppError")
	require.Equal(t, expectedCode, appErr.Code, "unexpected status code")
	require.Equal(t, expectedMessageKey, appErr.MessageKey, "unexpected message key")
}

// AssertAppErrorCode asserts that an error is an AppError with a specific status code
func AssertAppErrorCode(t *testing.T, err error, expectedCode int) {
	require.Error(t, err, "expected an error")

	var appErr *apperrors.AppError
	require.ErrorAs(t, err, &appErr, "expected an AppError")
	require.Equal(t, expectedCode, appErr.Code, "unexpected status code")
}
