package main

import (
	"bookhoard/internal/database"
	"bookhoard/internal/handlers"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestDeviceRegistrationFlow(t *testing.T) {
	setup := setupTestServer(t)

	// Step 1: Initiate device registration
	regRequest := map[string]interface{}{
		"device_name":       "Test Kindle Paperwhite",
		"device_type":       "koreader",
		"device_identifier": "kindle-test-hw-id-12345",
	}
	regBody, _ := json.Marshal(regRequest)

	req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration")

	var regResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &regResponse)

	registrationID, ok := regResponse["registration_id"].(string)
	assert.True(t, ok, "Should have registration_id")
	assert.NotEmpty(t, registrationID, "Registration ID should not be empty")

	authURL, ok := regResponse["auth_url"].(string)
	assert.True(t, ok, "Should have auth_url")
	assert.NotEmpty(t, authURL, "Auth URL should not be empty")

	// Step 2: Check registration status (should be pending initially)
	statusRequest := map[string]interface{}{
		"registration_id": registrationID,
	}
	statusBody, _ := json.Marshal(statusRequest)

	req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody))
	req.Header.Set("Content-Type", "application/json")
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status")

	var statusResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &statusResponse)

	status, ok := statusResponse["status"].(string)
	assert.True(t, ok, "Should have status")
	assert.Equal(t, "pending", status, "Should be pending initially")

	// Step 3: Login as user to approve device
	loginRequest := map[string]interface{}{
		"login":    "testuser@tests.bookhoard.internal",
		"password": "Test@Pass123!",
	}
	loginBody, _ := json.Marshal(loginRequest)

	req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginBody))
	req.Header.Set("Content-Type", "application/json")
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should login successfully")

	var loginResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &loginResponse)

	token, ok := loginResponse["access_token"].(string)
	assert.True(t, ok, "Should have access_token")

	// Step 4: Approve the device
	req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should approve device")

	// Step 5: Check registration status again (should be approved now)
	req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody))
	req.Header.Set("Content-Type", "application/json")
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status after approval")

	var approvedStatus map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &approvedStatus)

	status, ok = approvedStatus["status"].(string)
	assert.True(t, ok, "Should have status")
	assert.Equal(t, "approved", status, "Should be approved after user approval")

	authToken, ok := approvedStatus["auth_token"].(string)
	assert.True(t, ok, "Should have auth_token after approval")
	assert.NotEmpty(t, authToken, "Auth token should not be empty")
}

func TestListDevices(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a device using the setup helper
	_ = setup.CreateDevice(t, "Test Device", "koreader", "test-device-123")

	// List devices
	req := httptest.NewRequest("GET", "/api/devices", nil)
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should list devices")

	var response handlers.DeviceListResponse
	err := json.Unmarshal(rec.Body.Bytes(), &response)
	require.NoError(t, err, "Should unmarshal device list response")
	assert.GreaterOrEqual(t, len(response.Devices), 1, "Should have at least one device")

	firstDevice := response.Devices[0]
	assert.Equal(t, "Test Device", firstDevice.DeviceName, "Should match created device name")
}

func TestUpdateDevice(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a device using the setup helper
	device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123")

	// Update device
	syncEnabled := false
	syncFreq := int32(10)
	updateRequest := handlers.DeviceUpdateRequest{
		DeviceName:           "Updated Device Name",
		SyncEnabled:          &syncEnabled,
		SyncFrequencyMinutes: &syncFreq,
	}
	updateBody, _ := json.Marshal(updateRequest)

	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s", device.ID.String()), bytes.NewReader(updateBody))
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should update device")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)

	assert.True(t, response["device_updated"].(bool), "Should confirm device updated")

	// NEW: Verify database state
	updatedDevice := response["device"].(map[string]interface{})
	assert.Equal(t, "Updated Device Name", updatedDevice["device_name"], "Should have updated name")

	// Verify in database
	pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true}
	dbDevice, err := setup.DB.GetDevice(context.Background(), pgDeviceID)
	require.NoError(t, err, "Should retrieve updated device")
	assert.Equal(t, "Updated Device Name", dbDevice.DeviceName, "DB should have updated name")
	assert.Equal(t, false, dbDevice.SyncEnabled.Bool, "DB should show sync disabled")
	assert.Equal(t, int32(10), dbDevice.SyncFrequencyMinutes.Int32, "DB should have updated frequency")
}

func TestDeleteDevice(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a device using the setup helper
	device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123")

	// Delete device
	req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/devices/%s", device.ID.String()), nil)
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device")

	// Verify device is deleted
	pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true}
	_, err := setup.DB.GetDevice(context.Background(), pgDeviceID)
	assert.Error(t, err, "Device should be deleted")
}

func TestDeviceAuthentication(t *testing.T) {
	setup := setupTestServer(t)

	// Create a device directly in the database
	userID := getTestUserID(t, setup.DB)

	deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
	_, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
		UserID:               pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
		DeviceName:           "Test Device",
		DeviceType:           "koreader",
		DeviceIdentifier:     "test-device-123",
		AuthToken:            deviceToken,
		SyncEnabled:          pgtype.Bool{Bool: true, Valid: true},
		AutoSync:             pgtype.Bool{Bool: true, Valid: true},
		SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
		DeviceMetadata:       []byte("{}"),
	})
	assert.NoError(t, err, "Should create device")

	// Test device authentication
	req := httptest.NewRequest("GET", "/api/devices", nil)
	req.Header.Set("Authorization", "Bearer "+deviceToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	// This should fail because device auth middleware is not applied to /api/devices
	// Device auth is for sync endpoints only
	assert.Equal(t, http.StatusUnauthorized, rec.Code, "Should require user auth for device management")
}

func TestListPendingRegistrations(t *testing.T) {
	setup := setupTestServer(t)

	req := httptest.NewRequest("GET", "/api/devices/pending", nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should list pending registrations")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)

	pending, ok := response["pending_registrations"].([]interface{})
	assert.True(t, ok, "Should have pending_registrations array")
	assert.NotNil(t, pending, "Pending registrations should not be nil")
}

func TestApproveDeviceRegistration(t *testing.T) {
	setup := setupTestServer(t)

	regRequest := map[string]interface{}{
		"device_name":       "Test Device for Approval",
		"device_type":       "koreader",
		"device_identifier": "test-approval-12345",
	}
	regBody, _ := json.Marshal(regRequest)

	req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration")

	var regResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &regResponse)

	registrationID, ok := regResponse["registration_id"].(string)
	assert.True(t, ok, "Should have registration_id")

	req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should approve device registration")

	var approveResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &approveResponse)

	assert.True(t, approveResponse["approved"].(bool), "Should confirm approval")
}

func TestRejectDeviceRegistration(t *testing.T) {
	setup := setupTestServer(t)

	regRequest := map[string]interface{}{
		"device_name":       "Test Device for Rejection",
		"device_type":       "koreader",
		"device_identifier": "test-rejection-12345",
	}
	regBody, _ := json.Marshal(regRequest)

	req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration")

	var regResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &regResponse)

	registrationID, ok := regResponse["registration_id"].(string)
	assert.True(t, ok, "Should have registration_id")

	req = httptest.NewRequest("POST", fmt.Sprintf("/api/devices/reject/%s", registrationID), nil)
	req.Header.Set("Authorization", "Bearer "+setup.Token)
	rec = httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should reject device registration")

	var rejectResponse map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &rejectResponse)

	assert.Equal(t, "device registration rejected", rejectResponse["message"], "Should confirm rejection message")
}

func TestRegenerateDeviceToken_Success(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a device
	device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123")
	oldToken := device.AuthToken

	// Regenerate token
	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device.ID.String()), nil)
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should regenerate token")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)

	assert.True(t, response["message"].(string) != "", "Should have success message")

	newToken, ok := response["auth_token"].(string)
	assert.True(t, ok, "Should have auth_token")
	assert.NotEmpty(t, newToken, "New token should not be empty")
	assert.NotEqual(t, oldToken, newToken, "New token should be different from old token")

	// Verify device info is returned
	deviceInfo, ok := response["device"].(map[string]interface{})
	assert.True(t, ok, "Should have device info")
	assert.Equal(t, "Test Device", deviceInfo["device_name"], "Should return device name")

	// Verify sync URLs are returned
	syncURLs, ok := response["sync_urls"].(map[string]interface{})
	assert.True(t, ok, "Should have sync_urls")
	assert.Contains(t, syncURLs, "progress", "Should have progress URL")
	assert.Contains(t, syncURLs, "metadata", "Should have metadata URL")
}

func TestRegenerateDeviceToken_OldTokenInvalidated(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a device
	device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-invalidated")

	// Test old token works initially
	req1 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil)
	req1.Header.Set("Authorization", "Bearer "+device.AuthToken)
	req1.Header.Set("Content-Type", "application/json")
	rec1 := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec1, req1)
	// May fail for other reasons (no data), but should not be unauthorized
	assert.NotEqual(t, http.StatusUnauthorized, rec1.Code, "Old token should work initially")

	// Regenerate token
	req2 := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device.ID.String()), nil)
	req2.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec2 := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec2, req2)
	assert.Equal(t, http.StatusOK, rec2.Code, "Should regenerate token")

	var response map[string]interface{}
	json.Unmarshal(rec2.Body.Bytes(), &response)
	newToken := response["auth_token"].(string)

	// Test old token no longer works
	req3 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil)
	req3.Header.Set("Authorization", "Bearer "+device.AuthToken)
	req3.Header.Set("Content-Type", "application/json")
	rec3 := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec3, req3)
	assert.Equal(t, http.StatusUnauthorized, rec3.Code, "Old token should be invalid after regeneration")

	// Test new token works
	req4 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil)
	req4.Header.Set("Authorization", "Bearer "+newToken)
	req4.Header.Set("Content-Type", "application/json")
	rec4 := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec4, req4)
	// May fail for other reasons, but should not be unauthorized
	assert.NotEqual(t, http.StatusUnauthorized, rec4.Code, "New token should work")
}

func TestRegenerateDeviceToken_Unauthorized(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-unauth")

	// Try to regenerate without JWT token
	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device.ID.String()), nil)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusUnauthorized, rec.Code, "Should require authentication")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)
	assert.Contains(t, response, "error", "Should return error message")
}

func TestRegenerateDeviceToken_Forbidden(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create device for user 1
	device1 := setup.CreateDevice(t, "User1 Device", "koreader", "user1-device")

	// Create a second user with different credentials
	ctx := context.Background()
	passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
	_, err := setup.DB.CreateUser(ctx, database.CreateUserParams{
		Email:        "differentuser@example.com",
		Username:     "differentuser",
		PasswordHash: passwordHash,
		FirstName:    pgtype.Text{String: "Different", Valid: true},
		LastName:     pgtype.Text{String: "User", Valid: true},
		Role:         "user",
	})
	assert.NoError(t, err)

	// Clean up the second user after the test
	defer func() {
		user2, err := setup.DB.GetUserByEmail(ctx, "differentuser@example.com")
		if err == nil {
			setup.DB.DeleteUser(ctx, user2.ID)
		}
	}()

	// Login as user 2
	loginRequest := map[string]interface{}{
		"login":    "differentuser@example.com",
		"password": "Test@Pass123!",
	}
	loginBody, _ := json.Marshal(loginRequest)
	loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginBody))
	loginReq.Header.Set("Content-Type", "application/json")
	loginRec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq)

	var loginResponse map[string]interface{}
	json.Unmarshal(loginRec.Body.Bytes(), &loginResponse)
	user2Token := loginResponse["access_token"].(string)

	// Try to regenerate user 1's device with user 2's token
	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device1.ID.String()), nil)
	req.Header.Set("Authorization", "Bearer "+user2Token)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusForbidden, rec.Code, "Should forbid access to other user's device")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)
	assert.Contains(t, response, "error", "Should return error message")
}

func TestRegenerateDeviceToken_NotFound(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	fakeDeviceID, _ := uuid.NewUUID()

	// Try to regenerate non-existent device
	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", fakeDeviceID.String()), nil)
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusNotFound, rec.Code, "Should return not found")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)
	assert.Contains(t, response, "error", "Should return error message")
}

func TestRegenerateDeviceToken_KoboDevice(t *testing.T) {
	setup := setupDeviceTest(t)
	defer setup.Server.Close()

	// Create a Kobo device
	koboDevice := setup.CreateDevice(t, "Test Kobo", "kobo", "test-kobo-regen")
	oldToken := koboDevice.AuthToken

	// Regenerate token
	req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", koboDevice.ID.String()), nil)
	req.Header.Set("Authorization", "Bearer "+setup.UserToken)
	rec := httptest.NewRecorder()
	setup.Server.Config.Handler.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code, "Should regenerate token")

	var response map[string]interface{}
	json.Unmarshal(rec.Body.Bytes(), &response)

	newToken := response["auth_token"].(string)
	assert.NotEqual(t, oldToken, newToken, "New token should be different")

	// Verify Kobo sync URLs are returned
	syncURLs, ok := response["sync_urls"].(map[string]interface{})
	assert.True(t, ok, "Should have sync_urls")
	assert.Contains(t, syncURLs, "sync_url", "Should have sync_url")
	assert.Contains(t, syncURLs, "markup", "Should have markup URL")
	assert.Contains(t, syncURLs, "bookmark", "Should have bookmark URL")
	assert.Contains(t, syncURLs, "init", "Should have init URL")

	// Verify URLs contain new token
	syncURL := syncURLs["sync_url"].(string)
	assert.Contains(t, syncURL, newToken, "Sync URL should contain new token")
	assert.NotContains(t, syncURL, oldToken, "Sync URL should not contain old token")
}
