// Package scoring provides importance score calculation for observations.
package scoring

import (
	"context"
	"sync"
	"testing"
	"time"

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

	"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
)

// MockObservationStore is a mock implementation of ObservationStore for testing.
type MockObservationStore struct {
	updateErr         error
	getErr            error
	getConceptsErr    error
	scores            map[int64]float64
	conceptWeights    map[string]float64
	observations      []*models.Observation
	updateScoresCalls int
	mu                sync.Mutex
}

func NewMockObservationStore() *MockObservationStore {
	return &MockObservationStore{
		observations:   []*models.Observation{},
		scores:         make(map[int64]float64),
		conceptWeights: make(map[string]float64),
	}
}

func (m *MockObservationStore) GetObservationsNeedingScoreUpdate(ctx context.Context, threshold time.Duration, limit int) ([]*models.Observation, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.getErr != nil {
		return nil, m.getErr
	}

	// Return observations that haven't been updated within threshold
	now := time.Now()
	var result []*models.Observation
	for _, obs := range m.observations {
		if !obs.ScoreUpdatedAt.Valid || now.Sub(time.Unix(obs.ScoreUpdatedAt.Int64, 0)) > threshold {
			result = append(result, obs)
			if len(result) >= limit {
				break
			}
		}
	}
	return result, nil
}

func (m *MockObservationStore) UpdateImportanceScores(ctx context.Context, scores map[int64]float64) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.updateScoresCalls++
	if m.updateErr != nil {
		return m.updateErr
	}

	for id, score := range scores {
		m.scores[id] = score
	}
	return nil
}

func (m *MockObservationStore) GetConceptWeights(ctx context.Context) (map[string]float64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.getConceptsErr != nil {
		return nil, m.getConceptsErr
	}
	return m.conceptWeights, nil
}

func (m *MockObservationStore) AddObservation(obs *models.Observation) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.observations = append(m.observations, obs)
}

func (m *MockObservationStore) SetConceptWeights(weights map[string]float64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.conceptWeights = weights
}

func (m *MockObservationStore) GetScore(id int64) (float64, bool) {
	m.mu.Lock()
	defer m.mu.Unlock()
	score, ok := m.scores[id]
	return score, ok
}

func (m *MockObservationStore) GetUpdateScoresCalls() int {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.updateScoresCalls
}

func (m *MockObservationStore) SetUpdateError(err error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.updateErr = err
}

func (m *MockObservationStore) SetGetError(err error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.getErr = err
}

// =============================================================================
// RECALCULATOR TESTS
// =============================================================================

func TestNewRecalculator(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()

	recalc := NewRecalculator(store, calc, log)

	require.NotNil(t, recalc)
	assert.NotNil(t, recalc.store)
	assert.NotNil(t, recalc.calculator)
	assert.Equal(t, 1*time.Hour, recalc.interval)
	assert.Equal(t, 500, recalc.batchSize)
	assert.False(t, recalc.running)
}

func TestRecalculator_RecalculateNow(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Add observations
	now := time.Now()
	store.AddObservation(&models.Observation{
		ID:             1,
		Type:           models.ObsTypeBugfix,
		CreatedAtEpoch: now.UnixMilli(),
	})
	store.AddObservation(&models.Observation{
		ID:             2,
		Type:           models.ObsTypeFeature,
		CreatedAtEpoch: now.Add(-7 * 24 * time.Hour).UnixMilli(),
	})

	ctx := context.Background()
	err := recalc.RecalculateNow(ctx)

	require.NoError(t, err)

	// Verify scores were calculated
	score1, ok := store.GetScore(1)
	assert.True(t, ok)
	assert.Greater(t, score1, 0.0)

	score2, ok := store.GetScore(2)
	assert.True(t, ok)
	assert.Greater(t, score2, 0.0)
	assert.Less(t, score2, score1, "older observation should have lower score")
}

func TestRecalculator_RefreshConceptWeights(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Set up custom weights in store
	customWeights := map[string]float64{
		"security":    0.50,
		"performance": 0.25,
	}
	store.SetConceptWeights(customWeights)

	ctx := context.Background()
	err := recalc.RefreshConceptWeights(ctx)

	require.NoError(t, err)

	// Verify weights were updated in calculator config
	config := calc.GetConfig()
	assert.Equal(t, 0.50, config.ConceptWeights["security"])
	assert.Equal(t, 0.25, config.ConceptWeights["performance"])
}

func TestRecalculator_RefreshConceptWeights_Error(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	store.getConceptsErr = assert.AnError

	ctx := context.Background()
	err := recalc.RefreshConceptWeights(ctx)

	require.Error(t, err)
}

func TestRecalculator_GetStats(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Set fields directly for testing
	recalc.interval = 2 * time.Hour
	recalc.batchSize = 250

	stats := recalc.GetStats()

	assert.False(t, stats.Running)
	assert.Equal(t, 2*time.Hour, stats.Interval)
	assert.Equal(t, 250, stats.BatchSize)
	assert.Equal(t, 7.0, stats.HalfLife)
	assert.Equal(t, 0.01, stats.MinScore)
}

func TestRecalculator_StartStop(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Use a short interval for testing
	recalc.interval = 50 * time.Millisecond

	// Add an observation
	store.AddObservation(&models.Observation{
		ID:             1,
		Type:           models.ObsTypeBugfix,
		CreatedAtEpoch: time.Now().UnixMilli(),
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Start in goroutine
	go recalc.Start(ctx)

	// Wait for initial run and at least one tick
	time.Sleep(100 * time.Millisecond)

	// Verify it ran
	calls := store.GetUpdateScoresCalls()
	assert.GreaterOrEqual(t, calls, 1, "should have run at least once")

	// Stop via context cancellation
	cancel()
	time.Sleep(100 * time.Millisecond)

	// Verify stopped
	stats := recalc.GetStats()
	assert.False(t, stats.Running)
}

func TestRecalculator_StartTwice(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	recalc.interval = 1 * time.Hour // Long interval so it doesn't tick during test

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Start first time
	go recalc.Start(ctx)
	time.Sleep(50 * time.Millisecond)

	// Try to start second time (should return immediately)
	go recalc.Start(ctx)
	time.Sleep(50 * time.Millisecond)

	// Should still be running (only once)
	stats := recalc.GetStats()
	assert.True(t, stats.Running)

	cancel()
}

func TestRecalculator_StopWhenNotRunning(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Stop without starting - should not panic
	recalc.Stop()

	stats := recalc.GetStats()
	assert.False(t, stats.Running)
}

func TestRecalculator_EmptyStore(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	ctx := context.Background()
	err := recalc.RecalculateNow(ctx)

	require.NoError(t, err)
	assert.Equal(t, 0, store.GetUpdateScoresCalls(), "should not call update with no observations")
}

func TestRecalculator_GetObservationsError(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	store.SetGetError(assert.AnError)

	ctx := context.Background()
	err := recalc.RecalculateNow(ctx)

	// Should not return error (logs it instead)
	require.NoError(t, err)
	assert.Equal(t, 0, store.GetUpdateScoresCalls())
}

func TestRecalculator_UpdateScoresError(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	store.AddObservation(&models.Observation{
		ID:             1,
		Type:           models.ObsTypeBugfix,
		CreatedAtEpoch: time.Now().UnixMilli(),
	})

	store.SetUpdateError(assert.AnError)

	ctx := context.Background()
	err := recalc.RecalculateNow(ctx)

	// Should not return error (logs it instead)
	require.NoError(t, err)
	assert.Equal(t, 1, store.GetUpdateScoresCalls())
}

func TestRecalculator_BatchProcessing(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Set small batch size
	recalc.batchSize = 3

	// Add 5 observations
	now := time.Now()
	for i := 1; i <= 5; i++ {
		store.AddObservation(&models.Observation{
			ID:             int64(i),
			Type:           models.ObsTypeBugfix,
			CreatedAtEpoch: now.UnixMilli(),
		})
	}

	ctx := context.Background()
	err := recalc.RecalculateNow(ctx)

	require.NoError(t, err)

	// Should only process batch size (3)
	scores := 0
	for i := 1; i <= 5; i++ {
		if _, ok := store.GetScore(int64(i)); ok {
			scores++
		}
	}
	assert.Equal(t, 3, scores, "should only process batch size observations")
}

func TestRecalculator_ConcurrentAccess(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	// Add observations
	now := time.Now()
	for i := 1; i <= 10; i++ {
		store.AddObservation(&models.Observation{
			ID:             int64(i),
			Type:           models.ObsTypeBugfix,
			CreatedAtEpoch: now.UnixMilli(),
		})
	}

	ctx := context.Background()
	var wg sync.WaitGroup

	// Run multiple recalculations concurrently
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = recalc.RecalculateNow(ctx)
		}()
	}

	wg.Wait()

	// Should complete without race conditions
	// (use -race flag to verify)
	assert.GreaterOrEqual(t, store.GetUpdateScoresCalls(), 1)
}

func TestRecalculator_StatsThreadSafe(t *testing.T) {
	store := NewMockObservationStore()
	calc := NewCalculator(nil)
	log := zerolog.Nop()
	recalc := NewRecalculator(store, calc, log)

	var wg sync.WaitGroup

	// Concurrent reads (use -race flag to verify)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = recalc.GetStats()
		}()
	}

	wg.Wait()
}
