package repositories

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

func TestResidenceRepository_Create(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")

	residence := &models.Residence{
		OwnerID:       user.ID,
		Name:          "Test House",
		StreetAddress: "123 Main St",
		City:          "Austin",
		StateProvince: "TX",
		PostalCode:    "78701",
		Country:       "USA",
		IsActive:      true,
		IsPrimary:     true,
	}

	err := repo.Create(residence)
	require.NoError(t, err)
	assert.NotZero(t, residence.ID)
}

func TestResidenceRepository_FindByID(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	found, err := repo.FindByID(residence.ID)
	require.NoError(t, err)
	assert.Equal(t, residence.ID, found.ID)
	assert.Equal(t, "Test House", found.Name)
	assert.Equal(t, user.ID, found.OwnerID)
}

func TestResidenceRepository_FindByID_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	_, err := repo.FindByID(9999)
	assert.Error(t, err)
}

func TestResidenceRepository_FindByID_InactiveNotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Deactivate
	db.Model(residence).Update("is_active", false)

	_, err := repo.FindByID(residence.ID)
	assert.Error(t, err) // Should not find inactive residences
}

func TestResidenceRepository_FindByUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")

	// Create residences
	r1 := testutil.CreateTestResidence(t, db, owner.ID, "House 1")
	r2 := testutil.CreateTestResidence(t, db, owner.ID, "House 2")

	// Share r1 with sharedUser
	repo.AddUser(r1.ID, sharedUser.ID)

	// Owner should see both
	ownerResidences, err := repo.FindByUser(owner.ID)
	require.NoError(t, err)
	assert.Len(t, ownerResidences, 2)

	// Shared user should see only r1
	sharedResidences, err := repo.FindByUser(sharedUser.ID)
	require.NoError(t, err)
	assert.Len(t, sharedResidences, 1)
	assert.Equal(t, r1.ID, sharedResidences[0].ID)

	// Another user should see nothing
	otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
	otherResidences, err := repo.FindByUser(otherUser.ID)
	require.NoError(t, err)
	assert.Len(t, otherResidences, 0)

	_ = r2 // suppress unused
}

func TestResidenceRepository_FindOwnedByUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	otherOwner := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")

	testutil.CreateTestResidence(t, db, owner.ID, "House 1")
	testutil.CreateTestResidence(t, db, owner.ID, "House 2")
	testutil.CreateTestResidence(t, db, otherOwner.ID, "Other House")

	residences, err := repo.FindOwnedByUser(owner.ID)
	require.NoError(t, err)
	assert.Len(t, residences, 2)
}

func TestResidenceRepository_Update(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	residence.Name = "Updated House"
	residence.City = "Dallas"
	err := repo.Update(residence)
	require.NoError(t, err)

	found, err := repo.FindByID(residence.ID)
	require.NoError(t, err)
	assert.Equal(t, "Updated House", found.Name)
	assert.Equal(t, "Dallas", found.City)
}

func TestResidenceRepository_Delete(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	err := repo.Delete(residence.ID)
	require.NoError(t, err)

	// Should not be found (soft delete sets is_active = false)
	_, err = repo.FindByID(residence.ID)
	assert.Error(t, err)
}

func TestResidenceRepository_HasAccess(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
	otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
	repo.AddUser(residence.ID, sharedUser.ID)

	tests := []struct {
		name     string
		userID   uint
		expected bool
	}{
		{"owner has access", owner.ID, true},
		{"shared user has access", sharedUser.ID, true},
		{"other user no access", otherUser.ID, false},
	}

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

func TestResidenceRepository_IsOwner(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
	repo.AddUser(residence.ID, sharedUser.ID)

	// Owner should be owner
	isOwner, err := repo.IsOwner(residence.ID, owner.ID)
	require.NoError(t, err)
	assert.True(t, isOwner)

	// Shared user should not be owner
	isOwner, err = repo.IsOwner(residence.ID, sharedUser.ID)
	require.NoError(t, err)
	assert.False(t, isOwner)
}

func TestResidenceRepository_AddAndRemoveUser(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")

	// Initially no access
	hasAccess, _ := repo.HasAccess(residence.ID, sharedUser.ID)
	assert.False(t, hasAccess)

	// Add user
	err := repo.AddUser(residence.ID, sharedUser.ID)
	require.NoError(t, err)

	hasAccess, _ = repo.HasAccess(residence.ID, sharedUser.ID)
	assert.True(t, hasAccess)

	// Remove user
	err = repo.RemoveUser(residence.ID, sharedUser.ID)
	require.NoError(t, err)

	hasAccess, _ = repo.HasAccess(residence.ID, sharedUser.ID)
	assert.False(t, hasAccess)
}

func TestResidenceRepository_GetResidenceUsers(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	user1 := testutil.CreateTestUser(t, db, "user1", "user1@test.com", "password")
	user2 := testutil.CreateTestUser(t, db, "user2", "user2@test.com", "password")

	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
	repo.AddUser(residence.ID, user1.ID)
	repo.AddUser(residence.ID, user2.ID)

	users, err := repo.GetResidenceUsers(residence.ID)
	require.NoError(t, err)
	assert.Len(t, users, 3) // owner + 2 shared users
}

func TestResidenceRepository_CountByOwner(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")

	testutil.CreateTestResidence(t, db, owner.ID, "House 1")
	testutil.CreateTestResidence(t, db, owner.ID, "House 2")
	testutil.CreateTestResidence(t, db, owner.ID, "House 3")

	count, err := repo.CountByOwner(owner.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(3), count)
}

func TestResidenceRepository_CreateShareCode(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")

	shareCode, err := repo.CreateShareCode(residence.ID, owner.ID, 24*time.Hour)
	require.NoError(t, err)
	assert.NotEmpty(t, shareCode.Code)
	assert.Len(t, shareCode.Code, 6)
	assert.True(t, shareCode.IsActive)
	assert.NotNil(t, shareCode.ExpiresAt)
}

func TestResidenceRepository_FindShareCodeByCode(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")

	created, err := repo.CreateShareCode(residence.ID, owner.ID, 24*time.Hour)
	require.NoError(t, err)

	found, err := repo.FindShareCodeByCode(created.Code)
	require.NoError(t, err)
	assert.Equal(t, created.ID, found.ID)
	assert.Equal(t, residence.ID, found.ResidenceID)
}

func TestResidenceRepository_FindShareCodeByCode_Expired(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")

	// Create expired code
	created, err := repo.CreateShareCode(residence.ID, owner.ID, -1*time.Hour) // Already expired
	require.NoError(t, err)

	_, err = repo.FindShareCodeByCode(created.Code)
	assert.Error(t, err) // Should fail for expired code
}

func TestResidenceRepository_GetAllResidenceTypes(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewResidenceRepository(db)

	// Create residence types
	types := []models.ResidenceType{
		{Name: "House"},
		{Name: "Apartment"},
		{Name: "Condo"},
	}
	for _, rt := range types {
		db.Create(&rt)
	}

	result, err := repo.GetAllResidenceTypes()
	require.NoError(t, err)
	assert.Len(t, result, 3)
}
