package deacon

import (
	"os"
	"path/filepath"
	"testing"
	"time"
)

func TestDefaultStuckConfig(t *testing.T) {
	config := DefaultStuckConfig()

	if config.PingTimeout != DefaultPingTimeout {
		t.Errorf("PingTimeout = %v, want %v", config.PingTimeout, DefaultPingTimeout)
	}
	if config.ConsecutiveFailures != DefaultConsecutiveFailures {
		t.Errorf("ConsecutiveFailures = %v, want %v", config.ConsecutiveFailures, DefaultConsecutiveFailures)
	}
	if config.Cooldown != DefaultCooldown {
		t.Errorf("Cooldown = %v, want %v", config.Cooldown, DefaultCooldown)
	}
}

func TestHealthCheckStateFile(t *testing.T) {
	path := HealthCheckStateFile("/tmp/test-town")
	expected := "/tmp/test-town/deacon/health-check-state.json"
	if path != expected {
		t.Errorf("HealthCheckStateFile = %q, want %q", path, expected)
	}
}

func TestLoadHealthCheckState_NonExistent(t *testing.T) {
	tmpDir := t.TempDir()

	state, err := LoadHealthCheckState(tmpDir)
	if err != nil {
		t.Fatalf("LoadHealthCheckState() error = %v", err)
	}
	if state.Agents == nil {
		t.Error("Agents map should be initialized")
	}
	if len(state.Agents) != 0 {
		t.Errorf("Expected empty agents map, got %d entries", len(state.Agents))
	}
}

func TestSaveAndLoadHealthCheckState(t *testing.T) {
	tmpDir := t.TempDir()

	// Create state with some data
	state := &HealthCheckState{
		Agents: map[string]*AgentHealthState{
			"gastown/polecats/max": {
				AgentID:             "gastown/polecats/max",
				ConsecutiveFailures: 2,
				ForceKillCount:      1,
			},
		},
	}

	// Save
	if err := SaveHealthCheckState(tmpDir, state); err != nil {
		t.Fatalf("SaveHealthCheckState() error = %v", err)
	}

	// Verify file exists
	stateFile := HealthCheckStateFile(tmpDir)
	if _, err := os.Stat(stateFile); os.IsNotExist(err) {
		t.Fatal("State file was not created")
	}

	// Load
	loaded, err := LoadHealthCheckState(tmpDir)
	if err != nil {
		t.Fatalf("LoadHealthCheckState() error = %v", err)
	}

	// Verify loaded data
	agent := loaded.Agents["gastown/polecats/max"]
	if agent == nil {
		t.Fatal("Agent not found in loaded state")
	}
	if agent.ConsecutiveFailures != 2 {
		t.Errorf("ConsecutiveFailures = %d, want 2", agent.ConsecutiveFailures)
	}
	if agent.ForceKillCount != 1 {
		t.Errorf("ForceKillCount = %d, want 1", agent.ForceKillCount)
	}
}

func TestGetAgentState(t *testing.T) {
	state := &HealthCheckState{}

	// First call creates the agent
	agent1 := state.GetAgentState("test/agent")
	if agent1 == nil {
		t.Fatal("GetAgentState returned nil")
	}
	if agent1.AgentID != "test/agent" {
		t.Errorf("AgentID = %q, want %q", agent1.AgentID, "test/agent")
	}

	// Second call returns same agent
	agent2 := state.GetAgentState("test/agent")
	if agent1 != agent2 {
		t.Error("GetAgentState should return the same pointer")
	}
}

func TestAgentHealthState_RecordPing(t *testing.T) {
	agent := &AgentHealthState{}

	before := time.Now()
	agent.RecordPing()
	after := time.Now()

	if agent.LastPingTime.Before(before) || agent.LastPingTime.After(after) {
		t.Error("LastPingTime should be set to current time")
	}
}

func TestAgentHealthState_RecordResponse(t *testing.T) {
	agent := &AgentHealthState{
		ConsecutiveFailures: 5,
	}

	before := time.Now()
	agent.RecordResponse()
	after := time.Now()

	if agent.LastResponseTime.Before(before) || agent.LastResponseTime.After(after) {
		t.Error("LastResponseTime should be set to current time")
	}
	if agent.ConsecutiveFailures != 0 {
		t.Errorf("ConsecutiveFailures should be reset to 0, got %d", agent.ConsecutiveFailures)
	}
}

func TestAgentHealthState_RecordFailure(t *testing.T) {
	agent := &AgentHealthState{
		ConsecutiveFailures: 2,
	}

	agent.RecordFailure()

	if agent.ConsecutiveFailures != 3 {
		t.Errorf("ConsecutiveFailures = %d, want 3", agent.ConsecutiveFailures)
	}
}

func TestAgentHealthState_RecordForceKill(t *testing.T) {
	agent := &AgentHealthState{
		ConsecutiveFailures: 5,
		ForceKillCount:      2,
	}

	before := time.Now()
	agent.RecordForceKill()
	after := time.Now()

	if agent.LastForceKillTime.Before(before) || agent.LastForceKillTime.After(after) {
		t.Error("LastForceKillTime should be set to current time")
	}
	if agent.ForceKillCount != 3 {
		t.Errorf("ForceKillCount = %d, want 3", agent.ForceKillCount)
	}
	if agent.ConsecutiveFailures != 0 {
		t.Errorf("ConsecutiveFailures should be reset to 0, got %d", agent.ConsecutiveFailures)
	}
}

func TestAgentHealthState_IsInCooldown(t *testing.T) {
	cooldown := 5 * time.Minute

	tests := []struct {
		name              string
		lastForceKillTime time.Time
		want              bool
	}{
		{
			name:              "no force-kill history",
			lastForceKillTime: time.Time{},
			want:              false,
		},
		{
			name:              "recently killed",
			lastForceKillTime: time.Now().Add(-1 * time.Minute),
			want:              true,
		},
		{
			name:              "cooldown expired",
			lastForceKillTime: time.Now().Add(-10 * time.Minute),
			want:              false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := &AgentHealthState{
				LastForceKillTime: tt.lastForceKillTime,
			}
			if got := agent.IsInCooldown(cooldown); got != tt.want {
				t.Errorf("IsInCooldown() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAgentHealthState_CooldownRemaining(t *testing.T) {
	cooldown := 5 * time.Minute

	tests := []struct {
		name              string
		lastForceKillTime time.Time
		wantZero          bool
	}{
		{
			name:              "no force-kill history",
			lastForceKillTime: time.Time{},
			wantZero:          true,
		},
		{
			name:              "recently killed",
			lastForceKillTime: time.Now().Add(-1 * time.Minute),
			wantZero:          false,
		},
		{
			name:              "cooldown expired",
			lastForceKillTime: time.Now().Add(-10 * time.Minute),
			wantZero:          true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := &AgentHealthState{
				LastForceKillTime: tt.lastForceKillTime,
			}
			got := agent.CooldownRemaining(cooldown)
			if tt.wantZero && got != 0 {
				t.Errorf("CooldownRemaining() = %v, want 0", got)
			}
			if !tt.wantZero && got == 0 {
				t.Error("CooldownRemaining() = 0, want non-zero")
			}
		})
	}
}

func TestAgentHealthState_ShouldForceKill(t *testing.T) {
	tests := []struct {
		name      string
		failures  int
		threshold int
		want      bool
	}{
		{
			name:      "below threshold",
			failures:  2,
			threshold: 3,
			want:      false,
		},
		{
			name:      "at threshold",
			failures:  3,
			threshold: 3,
			want:      true,
		},
		{
			name:      "above threshold",
			failures:  5,
			threshold: 3,
			want:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := &AgentHealthState{
				ConsecutiveFailures: tt.failures,
			}
			if got := agent.ShouldForceKill(tt.threshold); got != tt.want {
				t.Errorf("ShouldForceKill() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSaveHealthCheckState_CreatesDirectory(t *testing.T) {
	tmpDir := t.TempDir()
	nestedDir := filepath.Join(tmpDir, "nonexistent", "deacon")

	state := &HealthCheckState{
		Agents: make(map[string]*AgentHealthState),
	}

	// Should create the directory structure
	if err := SaveHealthCheckState(filepath.Join(tmpDir, "nonexistent"), state); err != nil {
		t.Fatalf("SaveHealthCheckState() error = %v", err)
	}

	// Verify directory was created
	if _, err := os.Stat(nestedDir); os.IsNotExist(err) {
		t.Error("Directory should have been created")
	}
}
