package services

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/labstack/echo/v4"
	"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 TestAuditService_LogEvent_WritesToDatabase(t *testing.T) {
	db := testutil.SetupTestDB(t)
	svc := NewAuditService(db)
	defer svc.Stop()

	// Create a fake Echo context
	e := echo.New()
	req := httptest.NewRequest(http.MethodPost, "/api/auth/login/", nil)
	req.Header.Set("User-Agent", "TestAgent/1.0")
	req.RemoteAddr = "192.168.1.1:12345"
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	userID := uint(42)
	svc.LogEvent(c, &userID, AuditEventLogin, map[string]interface{}{
		"method": "password",
	})

	// Stop flushes the channel
	svc.Stop()

	var entries []models.AuditLog
	err := db.Find(&entries).Error
	require.NoError(t, err)
	require.Len(t, entries, 1)

	entry := entries[0]
	assert.Equal(t, uint(42), *entry.UserID)
	assert.Equal(t, AuditEventLogin, entry.EventType)
	assert.Equal(t, "TestAgent/1.0", entry.UserAgent)
	assert.NotEmpty(t, entry.IPAddress)
	assert.Equal(t, "password", entry.Details["method"])
	assert.False(t, entry.CreatedAt.IsZero())
}

func TestAuditService_LogEvent_NilUserID(t *testing.T) {
	db := testutil.SetupTestDB(t)
	svc := NewAuditService(db)
	defer svc.Stop()

	e := echo.New()
	req := httptest.NewRequest(http.MethodPost, "/api/auth/login/", nil)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	svc.LogEvent(c, nil, AuditEventLoginFailed, map[string]interface{}{
		"identifier": "unknown@test.com",
	})

	svc.Stop()

	var entries []models.AuditLog
	err := db.Find(&entries).Error
	require.NoError(t, err)
	require.Len(t, entries, 1)

	assert.Nil(t, entries[0].UserID)
	assert.Equal(t, AuditEventLoginFailed, entries[0].EventType)
}

func TestAuditService_LogEvent_NilContext(t *testing.T) {
	db := testutil.SetupTestDB(t)
	svc := NewAuditService(db)
	defer svc.Stop()

	userID := uint(1)
	svc.LogEvent(nil, &userID, AuditEventLogout, nil)

	svc.Stop()

	var entries []models.AuditLog
	err := db.Find(&entries).Error
	require.NoError(t, err)
	require.Len(t, entries, 1)

	assert.Equal(t, AuditEventLogout, entries[0].EventType)
	assert.Empty(t, entries[0].IPAddress)
	assert.Empty(t, entries[0].UserAgent)
}

func TestAuditService_LogEvent_MultipleEvents(t *testing.T) {
	db := testutil.SetupTestDB(t)
	svc := NewAuditService(db)
	defer svc.Stop()

	e := echo.New()
	req := httptest.NewRequest(http.MethodPost, "/", nil)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	userID := uint(10)
	svc.LogEvent(c, &userID, AuditEventRegister, nil)
	svc.LogEvent(c, &userID, AuditEventLogin, nil)
	svc.LogEvent(c, &userID, AuditEventLogout, nil)

	svc.Stop()

	var count int64
	err := db.Model(&models.AuditLog{}).Count(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(3), count)
}

func TestAuditService_EventTypeConstants(t *testing.T) {
	// Verify all event constants have expected values
	assert.Equal(t, "auth.login", AuditEventLogin)
	assert.Equal(t, "auth.login_failed", AuditEventLoginFailed)
	assert.Equal(t, "auth.register", AuditEventRegister)
	assert.Equal(t, "auth.logout", AuditEventLogout)
	assert.Equal(t, "auth.password_reset", AuditEventPasswordReset)
	assert.Equal(t, "auth.password_changed", AuditEventPasswordChanged)
	assert.Equal(t, "auth.account_deleted", AuditEventAccountDeleted)
}

func TestAuditService_Stop_FlushesRemainingEntries(t *testing.T) {
	db := testutil.SetupTestDB(t)
	svc := NewAuditService(db)

	e := echo.New()
	req := httptest.NewRequest(http.MethodPost, "/", nil)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	// Send many events quickly
	for i := 0; i < 50; i++ {
		uid := uint(i)
		svc.LogEvent(c, &uid, AuditEventLogin, nil)
	}

	// Stop should block until all entries are written
	svc.Stop()

	var count int64
	err := db.Model(&models.AuditLog{}).Count(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(50), count)
}

func TestAuditLog_TableName(t *testing.T) {
	log := models.AuditLog{}
	assert.Equal(t, "audit_log", log.TableName())
}

func TestAuditLog_JSONMap_NilHandling(t *testing.T) {
	db := testutil.SetupTestDB(t)

	// Create entry with nil details
	entry := &models.AuditLog{
		EventType: "test",
		CreatedAt: time.Now().UTC(),
	}
	err := db.Create(entry).Error
	require.NoError(t, err)

	// Read it back
	var found models.AuditLog
	err = db.First(&found, entry.ID).Error
	require.NoError(t, err)
	assert.Nil(t, found.Details)
}
