package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/bcrypt"
)

func TestUpdateProfileSelfEdit(t *testing.T) {
	t.Run("PUT /api/auth/profile - Update all fields successfully", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		updateData := map[string]interface{}{
			"username":   "updateduser",
			"email":      "updated@tests.bookhoard.internal",
			"first_name": "Updated",
			"last_name":  "Name",
			"theme":      "dracula",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		user, err := setup.DB.GetUserByUsername(context.Background(), "updateduser")
		require.NoError(t, err)
		assert.Equal(t, "updated@tests.bookhoard.internal", user.Email)
		assert.Equal(t, "Updated", user.FirstName.String)
		assert.Equal(t, "Name", user.LastName.String)
		assert.Equal(t, "dracula", user.Theme.String)
	})

	t.Run("PUT /api/auth/profile - Update username conflict", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		updateData := map[string]interface{}{
			"username": "testuser",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusConflict, rec.Code)
		assert.Contains(t, rec.Body.String(), "username already taken")
	})

	t.Run("PUT /api/auth/profile - Update email conflict", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		updateData := map[string]interface{}{
			"email": "testuser@tests.bookhoard.internal",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusConflict, rec.Code)
		assert.Contains(t, rec.Body.String(), "email already taken")
	})

	t.Run("PUT /api/auth/profile - Regular user tries to set role", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		updateData := map[string]interface{}{
			"role": "admin",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		user, err := setup.DB.GetUser(context.Background(), uuidToPGType(regularUser.ID))
		require.NoError(t, err)
		assert.Equal(t, "user", user.Role)
	})

	t.Run("PUT /api/auth/profile - Partial update (first name only)", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		updateData := map[string]interface{}{
			"first_name": "OnlyFirstName",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)
	})

	t.Run("PUT /api/auth/profile - No auth", func(t *testing.T) {
		setup := setupTestServer(t)

		updateData := map[string]interface{}{
			"first_name": "NoAuth",
		}
		jsonData, _ := json.Marshal(updateData)

		req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusUnauthorized, rec.Code)
	})
}

func TestUpdateProfileAdminMode(t *testing.T) {
	t.Run("PUT /api/auth/profile/:id - Admin update username", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		newUsername := "updateduser-admin-test"
		jsonData, _ := json.Marshal(map[string]interface{}{
			"username": newUsername,
		})

		req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		updatedUser, err := setup.DB.GetUserByUsername(context.Background(), newUsername)
		require.NoError(t, err)
		assert.Equal(t, newUsername, updatedUser.Username)
	})

	t.Run("PUT /api/auth/profile/:id - Admin promote user to admin", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"role": "admin",
		})

		req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		updatedUser, err := setup.DB.GetUser(context.Background(), uuidToPGType(targetUser.ID))
		require.NoError(t, err)
		assert.Equal(t, "admin", updatedUser.Role)
	})

	t.Run("PUT /api/auth/profile/:id - Try to demote last admin", func(t *testing.T) {
		setup := setupTestServer(t)

		// Delete the regular user so we have only 1 admin (testuser)
		regularUser := createRegularUserOnce(t, setup.DB)
		err := setup.DB.DeleteUser(context.Background(), uuidToPGType(regularUser.ID))
		require.NoError(t, err, "Failed to delete regular user")

		// Verify we have only 1 admin
		users, _ := setup.DB.ListUsers(context.Background())
		adminCount := 0
		for _, u := range users {
			if u.Role == "admin" {
				adminCount++
			}
		}
		require.Equal(t, 1, adminCount, "Expected exactly 1 admin after deleting regular user")

		// Now try to demote the only remaining admin
		lastAdmin := createTestUserOnce(t, setup.DB)
		token := setup.Token

		jsonData, _ := json.Marshal(map[string]interface{}{
			"role": "user",
		})

		req := httptest.NewRequest("PUT", "/api/auth/profile/"+lastAdmin.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "cannot demote the last admin")
	})

	t.Run("PUT /api/auth/profile/:id - Non-admin tries update", func(t *testing.T) {
		setup := setupTestServer(t)

		targetAdmin := createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"username": "hacked",
		})

		req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetAdmin.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusForbidden, rec.Code)
	})

	t.Run("PUT /api/auth/profile/:id - Invalid role", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"role": "superadmin",
		})

		req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "oneof")
	})
}

func TestUpdatePasswordAdminMode(t *testing.T) {
	t.Run("PUT /api/auth/password/:id - Admin reset password", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		newPassword := "NewPassword123!"
		jsonData, _ := json.Marshal(map[string]interface{}{
			"new_password":     newPassword,
			"confirm_password": newPassword,
		})

		req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		loginData, _ := json.Marshal(map[string]interface{}{
			"login":    targetUser.Username,
			"password": newPassword,
		})

		loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginData))
		loginReq.Header.Set("Content-Type", "application/json")

		loginRec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq)

		assert.Equal(t, http.StatusOK, loginRec.Code)
	})

	t.Run("PUT /api/auth/password/:id - Non-admin tries reset", func(t *testing.T) {
		setup := setupTestServer(t)

		targetAdmin := createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"new_password":     "hacked",
			"confirm_password": "hacked",
		})

		req := httptest.NewRequest("PUT", "/api/auth/password/"+targetAdmin.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusForbidden, rec.Code)
	})

	t.Run("PUT /api/auth/password/:id - Mismatched passwords", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"new_password":     "NewPassword123!",
			"confirm_password": "DifferentPass123!",
		})

		req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "passwords do not match")
	})

	t.Run("PUT /api/auth/password/:id - Invalid password format", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		jsonData, _ := json.Marshal(map[string]interface{}{
			"new_password":     "123",
			"confirm_password": "123",
		})

		req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
	})
}

func TestDeleteUserConsolidated(t *testing.T) {
	t.Run("DELETE /api/auth/profile - User deletes self successfully", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)

		selfDeletingUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, selfDeletingUser.Email, selfDeletingUser.Password)

		req := httptest.NewRequest("DELETE", "/api/auth/profile", nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		_, err := setup.DB.GetUser(context.Background(), uuidToPGType(selfDeletingUser.ID))
		assert.Error(t, err)
	})

	t.Run("DELETE /api/auth/profile - Delete without auth", func(t *testing.T) {
		setup := setupTestServer(t)

		req := httptest.NewRequest("DELETE", "/api/auth/profile", nil)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusUnauthorized, rec.Code)
	})

	t.Run("DELETE /api/auth/profile - Last admin tries self-delete", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		req := httptest.NewRequest("DELETE", "/api/auth/profile", nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "cannot delete the last admin")
	})

	t.Run("DELETE /api/auth/profile/:id - Admin deletes user", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		targetUser := createRegularUserOnce(t, setup.DB)

		req := httptest.NewRequest("DELETE", "/api/auth/profile/"+targetUser.ID.String(), nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		_, err := setup.DB.GetUser(context.Background(), uuidToPGType(targetUser.ID))
		assert.Error(t, err)
	})

	t.Run("DELETE /api/auth/profile/:id - Admin tries delete last admin", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		users, err := setup.DB.ListUsers(context.Background())
		require.NoError(t, err)
		var lastAdminID string
		for _, u := range users {
			if u.Role == "admin" {
				lastAdminID = uuid.UUID(u.ID.Bytes).String()
				break
			}
		}

		req := httptest.NewRequest("DELETE", "/api/auth/profile/"+lastAdminID, nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "cannot delete the last admin")
	})

	t.Run("DELETE /api/auth/profile/:id - Non-admin tries delete", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		regularUser := createRegularUserOnce(t, setup.DB)
		token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password)

		users, err := setup.DB.ListUsers(context.Background())
		require.NoError(t, err)
		var adminID string
		for _, u := range users {
			if u.Role == "admin" {
				adminID = uuid.UUID(u.ID.Bytes).String()
				break
			}
		}

		req := httptest.NewRequest("DELETE", "/api/auth/profile/"+adminID, nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusForbidden, rec.Code)
	})

	t.Run("DELETE /api/auth/profile/:id - Delete non-existent user", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		fakeUUID := uuid.New()
		req := httptest.NewRequest("DELETE", "/api/auth/profile/"+fakeUUID.String(), nil)
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusNotFound, rec.Code)
	})
}

func TestUpdatePasswordSelfChange(t *testing.T) {
	t.Run("PUT /api/auth/password - Successful password change", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		newPassword := "NewTest@Pass123!"
		jsonData, _ := json.Marshal(map[string]interface{}{
			"current_password": "Test@Pass123!",
			"new_password":     newPassword,
			"confirm_password": newPassword,
		})

		req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)

		loginData, _ := json.Marshal(map[string]interface{}{
			"login":    "testuser",
			"password": newPassword,
		})

		loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginData))
		loginReq.Header.Set("Content-Type", "application/json")

		loginRec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq)

		assert.Equal(t, http.StatusOK, loginRec.Code)
	})

	t.Run("PUT /api/auth/password - Wrong current password", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		jsonData, _ := json.Marshal(map[string]interface{}{
			"current_password": "wrongpassword",
			"new_password":     "NewTest@Pass123!",
			"confirm_password": "NewTest@Pass123!",
		})

		req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusUnauthorized, rec.Code)
		assert.Contains(t, rec.Body.String(), "current password is incorrect")
	})

	t.Run("PUT /api/auth/password - Missing current password", func(t *testing.T) {
		setup := setupTestServer(t)

		_ = createTestUserOnce(t, setup.DB)
		token := setup.Token

		jsonData, _ := json.Marshal(map[string]interface{}{
			"new_password":     "NewTest@Pass123!",
			"confirm_password": "NewTest@Pass123!",
		})

		req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)

		rec := httptest.NewRecorder()
		setup.Server.Config.Handler.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadRequest, rec.Code)
		assert.Contains(t, rec.Body.String(), "current password required")
	})
}

func verifyPasswordHash(t *testing.T, hashedPassword, password string) {
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
	assert.NoError(t, err, "Password should match hash")
}
