package main

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

	"github.com/steveyegge/beads/internal/storage/memory"
)

func TestTipSelection(t *testing.T) {
	// Set deterministic seed for testing
	os.Setenv("BEADS_TIP_SEED", "12345")
	defer os.Unsetenv("BEADS_TIP_SEED")

	// Reset RNG
	tipRandOnce = sync.Once{}
	initTipRand()

	// Reset tip registry for testing
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()

	store := memory.New("")

	// Test 1: No tips registered
	tip := selectNextTip(store)
	if tip != nil {
		t.Errorf("Expected nil with no tips registered, got %v", tip)
	}

	// Test 2: Single tip with condition = true
	tipsMutex.Lock()
	tips = append(tips, Tip{
		ID:          "test_tip_1",
		Condition:   func() bool { return true },
		Message:     "Test tip 1",
		Frequency:   1 * time.Hour,
		Priority:    100,
		Probability: 1.0, // Always show
	})
	tipsMutex.Unlock()

	tip = selectNextTip(store)
	if tip == nil {
		t.Fatal("Expected tip to be selected")
	}
	if tip.ID != "test_tip_1" {
		t.Errorf("Expected tip ID 'test_tip_1', got %q", tip.ID)
	}

	// Test 3: Frequency limit - should not show again immediately
	recordTipShown(store, "test_tip_1")
	tip = selectNextTip(store)
	if tip != nil {
		t.Errorf("Expected nil due to frequency limit, got %v", tip)
	}

	// Test 4: Multiple tips - priority order
	tipsMutex.Lock()
	tips = []Tip{
		{
			ID:          "low_priority",
			Condition:   func() bool { return true },
			Message:     "Low priority tip",
			Frequency:   1 * time.Hour,
			Priority:    10,
			Probability: 1.0,
		},
		{
			ID:          "high_priority",
			Condition:   func() bool { return true },
			Message:     "High priority tip",
			Frequency:   1 * time.Hour,
			Priority:    100,
			Probability: 1.0,
		},
	}
	tipsMutex.Unlock()

	tip = selectNextTip(store)
	if tip == nil {
		t.Fatal("Expected tip to be selected")
	}
	if tip.ID != "high_priority" {
		t.Errorf("Expected high_priority tip to be selected first, got %q", tip.ID)
	}

	// Test 5: Condition = false
	tipsMutex.Lock()
	tips = []Tip{
		{
			ID:          "never_show",
			Condition:   func() bool { return false },
			Message:     "Never shown",
			Frequency:   1 * time.Hour,
			Priority:    100,
			Probability: 1.0,
		},
	}
	tipsMutex.Unlock()

	tip = selectNextTip(store)
	if tip != nil {
		t.Errorf("Expected nil due to condition=false, got %v", tip)
	}
}

func TestTipProbability(t *testing.T) {
	// Set deterministic seed
	os.Setenv("BEADS_TIP_SEED", "99999")
	defer os.Unsetenv("BEADS_TIP_SEED")

	// Reset RNG by creating a new Once
	tipRandOnce = sync.Once{}
	initTipRand()

	tipsMutex.Lock()
	tips = []Tip{
		{
			ID:          "rare_tip",
			Condition:   func() bool { return true },
			Message:     "Rare tip",
			Frequency:   1 * time.Hour,
			Priority:    100,
			Probability: 0.01, // 1% chance
		},
	}
	tipsMutex.Unlock()

	store := memory.New("")

	// Run selection multiple times
	shownCount := 0
	for i := 0; i < 100; i++ {
		// Clear last shown timestamp to make tip eligible
		_ = store.SetMetadata(context.Background(), "tip_rare_tip_last_shown", "")

		tip := selectNextTip(store)
		if tip != nil {
			shownCount++
		}
	}

	// With 1% probability, we expect ~1 show out of 100
	// Allow some variance (0-10 is reasonable for low probability)
	if shownCount > 10 {
		t.Errorf("Expected ~1 tip shown with 1%% probability, got %d", shownCount)
	}
}

func TestGetLastShown(t *testing.T) {
	store := memory.New("")

	// Test 1: Never shown
	lastShown := getLastShown(store, "never_shown")
	if !lastShown.IsZero() {
		t.Errorf("Expected zero time for never shown tip, got %v", lastShown)
	}

	// Test 2: Recently shown
	now := time.Now()
	_ = store.SetMetadata(context.Background(), "tip_test_last_shown", now.Format(time.RFC3339))

	lastShown = getLastShown(store, "test")
	if lastShown.IsZero() {
		t.Error("Expected non-zero time for shown tip")
	}

	// Should be within 1 second (accounting for rounding)
	diff := now.Sub(lastShown)
	if diff < 0 {
		diff = -diff
	}
	if diff > time.Second {
		t.Errorf("Expected last shown time to be close to now, got diff %v", diff)
	}
}

func TestRecordTipShown(t *testing.T) {
	store := memory.New("")

	recordTipShown(store, "test_tip")

	// Verify it was recorded
	lastShown := getLastShown(store, "test_tip")
	if lastShown.IsZero() {
		t.Error("Expected tip to be recorded as shown")
	}

	// Should be very recent
	if time.Since(lastShown) > time.Second {
		t.Errorf("Expected recent timestamp, got %v", lastShown)
	}
}

func TestMaybeShowTip_RespectsFlags(t *testing.T) {
	// Set deterministic seed
	os.Setenv("BEADS_TIP_SEED", "54321")
	defer os.Unsetenv("BEADS_TIP_SEED")

	tipsMutex.Lock()
	tips = []Tip{
		{
			ID:          "always_show",
			Condition:   func() bool { return true },
			Message:     "Always show tip",
			Frequency:   1 * time.Hour,
			Priority:    100,
			Probability: 1.0,
		},
	}
	tipsMutex.Unlock()

	store := memory.New("")

	// Test 1: Should not show in JSON mode
	jsonOutput = true
	maybeShowTip(store) // Should not panic or show output
	jsonOutput = false

	// Test 2: Should not show in quiet mode
	quietFlag = true
	maybeShowTip(store) // Should not panic or show output
	quietFlag = false

	// Test 3: Should show in normal mode (no assertions, just testing it doesn't panic)
	maybeShowTip(store)
}

func TestTipFrequency(t *testing.T) {
	store := memory.New("")

	tipsMutex.Lock()
	tips = []Tip{
		{
			ID:          "frequent_tip",
			Condition:   func() bool { return true },
			Message:     "Frequent tip",
			Frequency:   5 * time.Second,
			Priority:    100,
			Probability: 1.0,
		},
	}
	tipsMutex.Unlock()

	// First selection should work
	tip := selectNextTip(store)
	if tip == nil {
		t.Fatal("Expected tip to be selected")
	}

	// Record it as shown
	recordTipShown(store, tip.ID)

	// Should not show again immediately (within frequency window)
	tip = selectNextTip(store)
	if tip != nil {
		t.Errorf("Expected nil due to frequency limit, got %v", tip)
	}

	// Manually set last shown to past (simulate time passing)
	past := time.Now().Add(-10 * time.Second)
	_ = store.SetMetadata(context.Background(), "tip_frequent_tip_last_shown", past.Format(time.RFC3339))

	// Should show again now
	tip = selectNextTip(store)
	if tip == nil {
		t.Error("Expected tip to be selected after frequency window passed")
	}
}

func TestInjectTip(t *testing.T) {
	// Reset tip registry for testing
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()

	store := memory.New("")

	// Set deterministic seed for testing
	os.Setenv("BEADS_TIP_SEED", "11111")
	defer os.Unsetenv("BEADS_TIP_SEED")
	tipRandOnce = sync.Once{}
	initTipRand()

	// Test 1: Inject a new tip
	InjectTip(
		"injected_tip_1",
		"This is an injected tip",
		80,
		1*time.Hour,
		1.0, // Always show when eligible
		func() bool { return true },
	)

	tipsMutex.RLock()
	tipCount := len(tips)
	tipsMutex.RUnlock()

	if tipCount != 1 {
		t.Errorf("Expected 1 tip, got %d", tipCount)
	}

	// Verify tip can be selected
	tip := selectNextTip(store)
	if tip == nil {
		t.Fatal("Expected injected tip to be selected")
	}
	if tip.ID != "injected_tip_1" {
		t.Errorf("Expected tip ID 'injected_tip_1', got %q", tip.ID)
	}
	if tip.Message != "This is an injected tip" {
		t.Errorf("Expected message 'This is an injected tip', got %q", tip.Message)
	}
	if tip.Priority != 80 {
		t.Errorf("Expected priority 80, got %d", tip.Priority)
	}

	// Test 2: Inject another tip and verify priority ordering
	InjectTip(
		"injected_tip_2",
		"Higher priority tip",
		100,
		1*time.Hour,
		1.0,
		func() bool { return true },
	)

	tipsMutex.RLock()
	tipCount = len(tips)
	tipsMutex.RUnlock()

	if tipCount != 2 {
		t.Errorf("Expected 2 tips, got %d", tipCount)
	}

	// Higher priority tip should be selected first
	tip = selectNextTip(store)
	if tip == nil {
		t.Fatal("Expected tip to be selected")
	}
	if tip.ID != "injected_tip_2" {
		t.Errorf("Expected higher priority tip 'injected_tip_2' to be selected first, got %q", tip.ID)
	}

	// Test 3: Update existing tip (same ID)
	InjectTip(
		"injected_tip_1",
		"Updated message",
		50, // Lower priority now
		2*time.Hour,
		0.5,
		func() bool { return true },
	)

	tipsMutex.RLock()
	tipCount = len(tips)
	var updatedTip *Tip
	for i := range tips {
		if tips[i].ID == "injected_tip_1" {
			updatedTip = &tips[i]
			break
		}
	}
	tipsMutex.RUnlock()

	if tipCount != 2 {
		t.Errorf("Expected 2 tips after update (no duplicate), got %d", tipCount)
	}
	if updatedTip == nil {
		t.Fatal("Expected to find updated tip")
	}
	if updatedTip.Message != "Updated message" {
		t.Errorf("Expected updated message, got %q", updatedTip.Message)
	}
	if updatedTip.Priority != 50 {
		t.Errorf("Expected updated priority 50, got %d", updatedTip.Priority)
	}
	if updatedTip.Frequency != 2*time.Hour {
		t.Errorf("Expected updated frequency 2h, got %v", updatedTip.Frequency)
	}
	if updatedTip.Probability != 0.5 {
		t.Errorf("Expected updated probability 0.5, got %v", updatedTip.Probability)
	}
}

func TestRemoveTip(t *testing.T) {
	// Reset tip registry for testing
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()

	// Add some tips
	InjectTip("tip_a", "Tip A", 100, time.Hour, 1.0, func() bool { return true })
	InjectTip("tip_b", "Tip B", 90, time.Hour, 1.0, func() bool { return true })
	InjectTip("tip_c", "Tip C", 80, time.Hour, 1.0, func() bool { return true })

	tipsMutex.RLock()
	tipCount := len(tips)
	tipsMutex.RUnlock()

	if tipCount != 3 {
		t.Fatalf("Expected 3 tips, got %d", tipCount)
	}

	// Test 1: Remove middle tip
	RemoveTip("tip_b")

	tipsMutex.RLock()
	tipCount = len(tips)
	var foundB bool
	for _, tip := range tips {
		if tip.ID == "tip_b" {
			foundB = true
			break
		}
	}
	tipsMutex.RUnlock()

	if tipCount != 2 {
		t.Errorf("Expected 2 tips after removal, got %d", tipCount)
	}
	if foundB {
		t.Error("Expected tip_b to be removed")
	}

	// Test 2: Remove non-existent tip (should be no-op)
	RemoveTip("tip_nonexistent")

	tipsMutex.RLock()
	tipCount = len(tips)
	tipsMutex.RUnlock()

	if tipCount != 2 {
		t.Errorf("Expected 2 tips after no-op removal, got %d", tipCount)
	}

	// Test 3: Remove remaining tips
	RemoveTip("tip_a")
	RemoveTip("tip_c")

	tipsMutex.RLock()
	tipCount = len(tips)
	tipsMutex.RUnlock()

	if tipCount != 0 {
		t.Errorf("Expected 0 tips after removing all, got %d", tipCount)
	}
}

func TestInjectTipConcurrency(t *testing.T) {
	// Reset tip registry for testing
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()

	// Test thread safety by injecting and removing tips concurrently
	var wg sync.WaitGroup
	const numGoroutines = 50

	// Inject tips concurrently
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			tipID := "concurrent_tip_" + string(rune('a'+id%26))
			InjectTip(tipID, "Message", 50, time.Hour, 0.5, func() bool { return true })
		}(i)
	}
	wg.Wait()

	// Remove some tips concurrently
	for i := 0; i < numGoroutines/2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			tipID := "concurrent_tip_" + string(rune('a'+id%26))
			RemoveTip(tipID)
		}(i)
	}
	wg.Wait()

	// If we got here without panics or deadlocks, the test passes
	// Just verify we can still access the tips
	tipsMutex.RLock()
	_ = len(tips)
	tipsMutex.RUnlock()
}

func TestIsClaudeDetected(t *testing.T) {
	// Save original env vars
	origClaudeCode := os.Getenv("CLAUDE_CODE")
	origAnthropicCli := os.Getenv("ANTHROPIC_CLI")
	defer func() {
		os.Setenv("CLAUDE_CODE", origClaudeCode)
		os.Setenv("ANTHROPIC_CLI", origAnthropicCli)
	}()

	// Clear env vars for clean testing
	os.Unsetenv("CLAUDE_CODE")
	os.Unsetenv("ANTHROPIC_CLI")

	// Test 1: Detection via CLAUDE_CODE env var
	os.Setenv("CLAUDE_CODE", "1")
	if !isClaudeDetected() {
		t.Error("Expected Claude detected with CLAUDE_CODE env var")
	}
	os.Unsetenv("CLAUDE_CODE")

	// Test 2: Detection via ANTHROPIC_CLI env var
	os.Setenv("ANTHROPIC_CLI", "1")
	if !isClaudeDetected() {
		t.Error("Expected Claude detected with ANTHROPIC_CLI env var")
	}
	os.Unsetenv("ANTHROPIC_CLI")

	// Test 3: Detection via ~/.claude directory
	// This depends on the test environment - if ~/.claude exists, it should detect
	// We can't easily control this without modifying the filesystem
	home, err := os.UserHomeDir()
	if err == nil {
		claudeDir := home + "/.claude"
		if _, err := os.Stat(claudeDir); err == nil {
			// ~/.claude exists, should detect
			if !isClaudeDetected() {
				t.Error("Expected Claude detected with ~/.claude directory present")
			}
		}
	}
}

func TestIsClaudeSetupComplete(t *testing.T) {
	// This test checks the logic without modifying the filesystem
	// The actual detection depends on the presence of files

	// Test that the function returns a boolean and doesn't panic
	result := isClaudeSetupComplete()
	// Just verify it returns without error
	_ = result

	// If running in an environment with Claude setup, verify detection
	// We'll check both global and project paths exist
	home, err := os.UserHomeDir()
	if err != nil {
		return // Skip if we can't get home dir
	}

	globalCommand := home + "/.claude/commands/prime_beads.md"
	globalHooksSession := home + "/.claude/hooks/sessionstart"
	globalHooksPreTool := home + "/.claude/hooks/PreToolUse"

	// Check if global setup exists
	if _, err := os.Stat(globalCommand); err == nil {
		if _, err := os.Stat(globalHooksSession); err == nil {
			if !isClaudeSetupComplete() {
				t.Error("Expected Claude setup complete with global hooks (sessionstart)")
			}
		} else if _, err := os.Stat(globalHooksPreTool); err == nil {
			if !isClaudeSetupComplete() {
				t.Error("Expected Claude setup complete with global hooks (PreToolUse)")
			}
		}
	}

	// Check project-level setup
	projectCommand := ".claude/commands/prime_beads.md"
	projectHooksSession := ".claude/hooks/sessionstart"
	projectHooksPreTool := ".claude/hooks/PreToolUse"

	if _, err := os.Stat(projectCommand); err == nil {
		if _, err := os.Stat(projectHooksSession); err == nil {
			if !isClaudeSetupComplete() {
				t.Error("Expected Claude setup complete with project hooks (sessionstart)")
			}
		} else if _, err := os.Stat(projectHooksPreTool); err == nil {
			if !isClaudeSetupComplete() {
				t.Error("Expected Claude setup complete with project hooks (PreToolUse)")
			}
		}
	}
}

func TestClaudeSetupTipRegistered(t *testing.T) {
	// Reset tip registry with fresh default tips
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()
	initDefaultTips()

	// Verify that the claude_setup tip is registered
	tipsMutex.RLock()
	defer tipsMutex.RUnlock()

	var found bool
	for _, tip := range tips {
		if tip.ID == "claude_setup" {
			found = true
			// Verify tip properties
			if tip.Priority != 100 {
				t.Errorf("Expected claude_setup priority 100, got %d", tip.Priority)
			}
			if tip.Frequency != 24*time.Hour {
				t.Errorf("Expected claude_setup frequency 24h, got %v", tip.Frequency)
			}
			if tip.Probability != 0.6 {
				t.Errorf("Expected claude_setup probability 0.6, got %v", tip.Probability)
			}
			break
		}
	}

	if !found {
		t.Error("Expected claude_setup tip to be registered")
	}
}

func TestClaudeSetupTipCondition(t *testing.T) {
	// Save original env vars
	origClaudeCode := os.Getenv("CLAUDE_CODE")
	defer os.Setenv("CLAUDE_CODE", origClaudeCode)

	// Reset tip registry with fresh default tips
	tipsMutex.Lock()
	tips = []Tip{}
	tipsMutex.Unlock()
	initDefaultTips()

	// Find the claude_setup tip
	tipsMutex.RLock()
	var claudeTip *Tip
	for i := range tips {
		if tips[i].ID == "claude_setup" {
			claudeTip = &tips[i]
			break
		}
	}
	tipsMutex.RUnlock()

	if claudeTip == nil {
		t.Fatal("claude_setup tip not found")
	}

	// Test: When Claude is not detected, condition should be false
	os.Unsetenv("CLAUDE_CODE")
	os.Unsetenv("ANTHROPIC_CLI")
	// Note: This test may pass or fail depending on ~/.claude existence
	// The important thing is that the condition function executes without error
	_ = claudeTip.Condition()

	// Test: When Claude is detected but setup might be complete
	// Set env var to simulate Claude environment
	os.Setenv("CLAUDE_CODE", "1")
	conditionResult := claudeTip.Condition()
	// If setup is complete, should be false; if not complete, should be true
	// Just verify it returns a valid boolean
	_ = conditionResult
}
