package models

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// setupModelsTestDB creates a minimal in-memory SQLite for model-level tests
// that require database interaction (e.g., BeforeCreate hooks).
func setupModelsTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	require.NoError(t, err)
	err = db.AutoMigrate(&User{}, &AuthToken{}, &UserProfile{})
	require.NoError(t, err)
	return db
}

// === Residence model tests ===

func TestResidence_GetAllUsers(t *testing.T) {
	owner := User{Username: "owner"}
	owner.ID = 1
	member1 := User{Username: "member1"}
	member1.ID = 2
	member2 := User{Username: "member2"}
	member2.ID = 3

	residence := &Residence{
		OwnerID: owner.ID,
		Owner:   owner,
		Users:   []User{member1, member2},
	}

	allUsers := residence.GetAllUsers()
	assert.Len(t, allUsers, 3)
	assert.Equal(t, "owner", allUsers[0].Username)
}

func TestResidence_HasAccess(t *testing.T) {
	owner := User{Username: "owner"}
	owner.ID = 1
	member := User{Username: "member"}
	member.ID = 2

	residence := &Residence{
		OwnerID: owner.ID,
		Owner:   owner,
		Users:   []User{member},
	}

	tests := []struct {
		name     string
		userID   uint
		expected bool
	}{
		{"owner has access", 1, true},
		{"member has access", 2, true},
		{"stranger no access", 99, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, residence.HasAccess(tt.userID))
		})
	}
}

func TestResidence_IsPrimaryOwner(t *testing.T) {
	residence := &Residence{OwnerID: 1}

	assert.True(t, residence.IsPrimaryOwner(1))
	assert.False(t, residence.IsPrimaryOwner(2))
}

// === Document model tests ===

func TestDocument_TableName_DocumentImage(t *testing.T) {
	di := DocumentImage{}
	assert.Equal(t, "task_documentimage", di.TableName())
}

func TestDocument_IsWarrantyExpiringSoon(t *testing.T) {
	future30 := time.Now().UTC().AddDate(0, 0, 15)
	future90 := time.Now().UTC().AddDate(0, 0, 60)
	past := time.Now().UTC().AddDate(0, 0, -5)

	tests := []struct {
		name     string
		doc      Document
		days     int
		expected bool
	}{
		{
			name:     "warranty expiring within threshold",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future30},
			days:     30,
			expected: true,
		},
		{
			name:     "warranty not expiring within threshold",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future90},
			days:     30,
			expected: false,
		},
		{
			name:     "warranty already expired",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &past},
			days:     30,
			expected: false,
		},
		{
			name:     "non-warranty document",
			doc:      Document{DocumentType: DocumentTypeGeneral, ExpiryDate: &future30},
			days:     30,
			expected: false,
		},
		{
			name:     "warranty with nil expiry",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: nil},
			days:     30,
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.doc.IsWarrantyExpiringSoon(tt.days))
		})
	}
}

func TestDocument_IsWarrantyExpired(t *testing.T) {
	past := time.Now().UTC().AddDate(0, 0, -5)
	future := time.Now().UTC().AddDate(0, 0, 30)

	tests := []struct {
		name     string
		doc      Document
		expected bool
	}{
		{
			name:     "expired warranty",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &past},
			expected: true,
		},
		{
			name:     "active warranty",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: &future},
			expected: false,
		},
		{
			name:     "non-warranty",
			doc:      Document{DocumentType: DocumentTypeGeneral, ExpiryDate: &past},
			expected: false,
		},
		{
			name:     "warranty nil expiry",
			doc:      Document{DocumentType: DocumentTypeWarranty, ExpiryDate: nil},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.doc.IsWarrantyExpired())
		})
	}
}

func TestDocumentType_Constants(t *testing.T) {
	// Verify document type constants have expected values
	assert.Equal(t, DocumentType("general"), DocumentTypeGeneral)
	assert.Equal(t, DocumentType("warranty"), DocumentTypeWarranty)
	assert.Equal(t, DocumentType("receipt"), DocumentTypeReceipt)
	assert.Equal(t, DocumentType("contract"), DocumentTypeContract)
	assert.Equal(t, DocumentType("insurance"), DocumentTypeInsurance)
	assert.Equal(t, DocumentType("manual"), DocumentTypeManual)
}

// === Notification model tests ===

func TestNotification_TableName(t *testing.T) {
	n := Notification{}
	assert.Equal(t, "notifications_notification", n.TableName())
}

func TestNotificationPreference_TableName(t *testing.T) {
	np := NotificationPreference{}
	assert.Equal(t, "notifications_notificationpreference", np.TableName())
}

func TestAPNSDevice_TableName(t *testing.T) {
	d := APNSDevice{}
	assert.Equal(t, "push_notifications_apnsdevice", d.TableName())
}

func TestGCMDevice_TableName(t *testing.T) {
	d := GCMDevice{}
	assert.Equal(t, "push_notifications_gcmdevice", d.TableName())
}

func TestNotification_MarkAsRead(t *testing.T) {
	n := &Notification{Read: false}

	n.MarkAsRead()
	assert.True(t, n.Read)
	assert.NotNil(t, n.ReadAt)
}

func TestNotification_MarkAsSent(t *testing.T) {
	n := &Notification{Sent: false}

	n.MarkAsSent()
	assert.True(t, n.Sent)
	assert.NotNil(t, n.SentAt)
}

func TestNotificationType_Constants(t *testing.T) {
	assert.Equal(t, NotificationType("task_due_soon"), NotificationTaskDueSoon)
	assert.Equal(t, NotificationType("task_overdue"), NotificationTaskOverdue)
	assert.Equal(t, NotificationType("task_completed"), NotificationTaskCompleted)
	assert.Equal(t, NotificationType("task_assigned"), NotificationTaskAssigned)
	assert.Equal(t, NotificationType("residence_shared"), NotificationResidenceShared)
	assert.Equal(t, NotificationType("warranty_expiring"), NotificationWarrantyExpiring)
}

// === AuthToken model tests ===

func TestAuthToken_BeforeCreate_GeneratesKey(t *testing.T) {
	db := setupModelsTestDB(t)

	user := &User{
		Username: "tokenuser",
		Email:    "token@test.com",
		Password: "dummy",
		IsActive: true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)

	token := &AuthToken{UserID: user.ID}
	err = db.Create(token).Error
	require.NoError(t, err)

	assert.NotEmpty(t, token.Key)
	assert.Len(t, token.Key, 40) // 20 bytes = 40 hex chars
	assert.False(t, token.Created.IsZero())
}

func TestAuthToken_BeforeCreate_PreservesExistingKey(t *testing.T) {
	db := setupModelsTestDB(t)

	user := &User{
		Username: "tokenuser",
		Email:    "token@test.com",
		Password: "dummy",
		IsActive: true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)

	existingKey := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
	token := &AuthToken{
		Key:    existingKey,
		UserID: user.ID,
	}
	err = db.Create(token).Error
	require.NoError(t, err)

	assert.Equal(t, existingKey, token.Key)
}

func TestGetOrCreateToken_CreatesNew(t *testing.T) {
	db := setupModelsTestDB(t)

	user := &User{
		Username: "newtoken",
		Email:    "newtoken@test.com",
		Password: "dummy",
		IsActive: true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)

	token, err := GetOrCreateToken(db, user.ID)
	require.NoError(t, err)
	assert.NotEmpty(t, token.Key)
	assert.Equal(t, user.ID, token.UserID)
}

func TestGetOrCreateToken_ReturnsExisting(t *testing.T) {
	db := setupModelsTestDB(t)

	user := &User{
		Username: "existingtoken",
		Email:    "existingtoken@test.com",
		Password: "dummy",
		IsActive: true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)

	token1, err := GetOrCreateToken(db, user.ID)
	require.NoError(t, err)

	token2, err := GetOrCreateToken(db, user.ID)
	require.NoError(t, err)

	assert.Equal(t, token1.Key, token2.Key)
}

// === User model additional tests ===

func TestUser_SetPassword_And_CheckPassword_Integration(t *testing.T) {
	user := &User{}
	err := user.SetPassword("Password123")
	require.NoError(t, err)

	assert.True(t, user.CheckPassword("Password123"))
	assert.False(t, user.CheckPassword("WrongPassword"))
	assert.False(t, user.CheckPassword(""))
	assert.False(t, user.CheckPassword("password123")) // case sensitive
}

// === Task model additional tests ===

func TestTask_IsOverdue_CancelledNotOverdue(t *testing.T) {
	yesterday := time.Now().UTC().AddDate(0, 0, -2)
	task := &Task{
		DueDate:     &yesterday,
		IsCancelled: true,
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsOverdue_ArchivedNotOverdue(t *testing.T) {
	yesterday := time.Now().UTC().AddDate(0, 0, -2)
	task := &Task{
		DueDate:    &yesterday,
		IsArchived: true,
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsOverdue_NoDueDateNotOverdue(t *testing.T) {
	task := &Task{
		DueDate: nil,
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsOverdue_CompletedNotOverdue(t *testing.T) {
	yesterday := time.Now().UTC().AddDate(0, 0, -2)
	task := &Task{
		DueDate:     &yesterday,
		NextDueDate: nil,
		Completions: []TaskCompletion{{CompletedAt: time.Now().UTC()}},
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsOverdue_CompletionCountNotOverdue(t *testing.T) {
	yesterday := time.Now().UTC().AddDate(0, 0, -2)
	task := &Task{
		DueDate:         &yesterday,
		NextDueDate:     nil,
		CompletionCount: 1,
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsOverdue_UsesNextDueDate(t *testing.T) {
	// DueDate is overdue, but NextDueDate is in the future
	pastDue := time.Now().UTC().AddDate(0, 0, -10)
	futureDue := time.Now().UTC().AddDate(0, 0, 10)
	task := &Task{
		DueDate:     &pastDue,
		NextDueDate: &futureDue,
	}
	assert.False(t, task.IsOverdue())
}

func TestTask_IsDueSoon_CancelledNotDueSoon(t *testing.T) {
	futureDue := time.Now().UTC().AddDate(0, 0, 5)
	task := &Task{
		DueDate:     &futureDue,
		IsCancelled: true,
	}
	assert.False(t, task.IsDueSoon(30))
}

func TestTask_IsDueSoon_NoDueDateNotDueSoon(t *testing.T) {
	task := &Task{
		DueDate: nil,
	}
	assert.False(t, task.IsDueSoon(30))
}

func TestTask_IsDueSoon_WithinThreshold(t *testing.T) {
	futureDue := time.Now().UTC().AddDate(0, 0, 5)
	task := &Task{
		DueDate: &futureDue,
	}
	assert.True(t, task.IsDueSoon(30))
	assert.True(t, task.IsDueSoon(10))
	assert.False(t, task.IsDueSoon(3))
}

func TestTask_IsDueSoon_CompletedNotDueSoon(t *testing.T) {
	futureDue := time.Now().UTC().AddDate(0, 0, 5)
	task := &Task{
		DueDate:     &futureDue,
		NextDueDate: nil,
		Completions: []TaskCompletion{{CompletedAt: time.Now().UTC()}},
	}
	assert.False(t, task.IsDueSoon(30))
}

func TestTaskCompletionImage_TableName(t *testing.T) {
	tci := TaskCompletionImage{}
	assert.Equal(t, "task_taskcompletionimage", tci.TableName())
}

// === Subscription model additional tests ===

func TestSubscription_TableNames(t *testing.T) {
	assert.Equal(t, "subscription_subscriptionsettings", SubscriptionSettings{}.TableName())
	assert.Equal(t, "subscription_usersubscription", UserSubscription{}.TableName())
	assert.Equal(t, "subscription_upgradetrigger", UpgradeTrigger{}.TableName())
	assert.Equal(t, "subscription_featurebenefit", FeatureBenefit{}.TableName())
	assert.Equal(t, "subscription_promotion", Promotion{}.TableName())
	assert.Equal(t, "subscription_tierlimits", TierLimits{}.TableName())
}

func TestSubscription_IsActive(t *testing.T) {
	future := time.Now().UTC().Add(24 * time.Hour)
	past := time.Now().UTC().Add(-24 * time.Hour)

	tests := []struct {
		name     string
		sub      *UserSubscription
		expected bool
	}{
		{
			name:     "pro with future expiry is active",
			sub:      &UserSubscription{Tier: TierPro, ExpiresAt: &future},
			expected: true,
		},
		{
			name:     "pro with nil expiry is active",
			sub:      &UserSubscription{Tier: TierPro, ExpiresAt: nil},
			expected: true,
		},
		{
			name:     "pro with past expiry is not active",
			sub:      &UserSubscription{Tier: TierPro, ExpiresAt: &past},
			expected: false,
		},
		{
			name:     "free with active trial is active",
			sub:      &UserSubscription{Tier: TierFree, TrialEnd: &future},
			expected: true,
		},
		{
			name:     "free without trial is not active",
			sub:      &UserSubscription{Tier: TierFree},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.sub.IsActive())
		})
	}
}

func TestSubscription_SubscriptionSource(t *testing.T) {
	sub := &UserSubscription{Platform: "ios"}
	assert.Equal(t, "ios", sub.SubscriptionSource())
}

func TestPromotion_IsCurrentlyActive(t *testing.T) {
	now := time.Now().UTC()

	tests := []struct {
		name     string
		promo    Promotion
		expected bool
	}{
		{
			name: "active promotion within dates",
			promo: Promotion{
				IsActive:  true,
				StartDate: now.Add(-1 * time.Hour),
				EndDate:   now.Add(1 * time.Hour),
			},
			expected: true,
		},
		{
			name: "inactive promotion",
			promo: Promotion{
				IsActive:  false,
				StartDate: now.Add(-1 * time.Hour),
				EndDate:   now.Add(1 * time.Hour),
			},
			expected: false,
		},
		{
			name: "promotion not yet started",
			promo: Promotion{
				IsActive:  true,
				StartDate: now.Add(1 * time.Hour),
				EndDate:   now.Add(2 * time.Hour),
			},
			expected: false,
		},
		{
			name: "promotion already ended",
			promo: Promotion{
				IsActive:  true,
				StartDate: now.Add(-2 * time.Hour),
				EndDate:   now.Add(-1 * time.Hour),
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tt.promo.IsCurrentlyActive())
		})
	}
}

func TestGetDefaultFreeLimits(t *testing.T) {
	limits := GetDefaultFreeLimits()
	assert.Equal(t, TierFree, limits.Tier)
	require.NotNil(t, limits.PropertiesLimit)
	require.NotNil(t, limits.TasksLimit)
	require.NotNil(t, limits.ContractorsLimit)
	require.NotNil(t, limits.DocumentsLimit)
	assert.Equal(t, 1, *limits.PropertiesLimit)
	assert.Equal(t, 10, *limits.TasksLimit)
	assert.Equal(t, 0, *limits.ContractorsLimit)
	assert.Equal(t, 0, *limits.DocumentsLimit)
}

func TestGetDefaultProLimits(t *testing.T) {
	limits := GetDefaultProLimits()
	assert.Equal(t, TierPro, limits.Tier)
	assert.Nil(t, limits.PropertiesLimit)
	assert.Nil(t, limits.TasksLimit)
	assert.Nil(t, limits.ContractorsLimit)
	assert.Nil(t, limits.DocumentsLimit)
}

// === ConfirmationCode additional tests ===

func TestConfirmationCode_TableName(t *testing.T) {
	cc := ConfirmationCode{}
	assert.Equal(t, "user_confirmationcode", cc.TableName())
}

// === PasswordResetCode additional tests ===

func TestPasswordResetCode_TableName(t *testing.T) {
	prc := PasswordResetCode{}
	assert.Equal(t, "user_passwordresetcode", prc.TableName())
}

// === Social Auth TableName tests ===

func TestAppleSocialAuth_TableName(t *testing.T) {
	a := AppleSocialAuth{}
	assert.Equal(t, "user_applesocialauth", a.TableName())
}

func TestGoogleSocialAuth_TableName(t *testing.T) {
	g := GoogleSocialAuth{}
	assert.Equal(t, "user_googlesocialauth", g.TableName())
}

// === BaseModel tests ===

func TestBaseModel_BeforeCreate(t *testing.T) {
	b := &BaseModel{}
	err := b.BeforeCreate(nil)
	require.NoError(t, err)

	assert.False(t, b.CreatedAt.IsZero())
	assert.False(t, b.UpdatedAt.IsZero())
}

func TestBaseModel_BeforeCreate_PreservesExisting(t *testing.T) {
	existingTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	b := &BaseModel{
		CreatedAt: existingTime,
		UpdatedAt: existingTime,
	}
	err := b.BeforeCreate(nil)
	require.NoError(t, err)

	assert.Equal(t, existingTime, b.CreatedAt)
	assert.Equal(t, existingTime, b.UpdatedAt)
}

func TestBaseModel_BeforeUpdate(t *testing.T) {
	oldTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	b := &BaseModel{
		UpdatedAt: oldTime,
	}
	err := b.BeforeUpdate(nil)
	require.NoError(t, err)

	assert.True(t, b.UpdatedAt.After(oldTime))
}
