package contacts

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/nugget/thane-ai-agent/internal/integrations/homeassistant"
	"github.com/nugget/thane-ai-agent/internal/runtime/agentctx"
)

type mockStateGetter struct {
	states map[string]*homeassistant.State
	err    error
}

func (m *mockStateGetter) GetState(_ context.Context, entityID string) (*homeassistant.State, error) {
	if m.err != nil {
		return nil, m.err
	}
	s, ok := m.states[entityID]
	if !ok {
		return nil, fmt.Errorf("entity not found: %s", entityID)
	}
	return s, nil
}

func TestNewPresenceTracker(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice", "person.bob"}, "America/Chicago", nil)

	ids := tracker.EntityIDs()
	if len(ids) != 2 {
		t.Fatalf("expected 2 entity IDs, got %d", len(ids))
	}
	if ids[0] != "person.alice" || ids[1] != "person.bob" {
		t.Errorf("unexpected entity IDs: %v", ids)
	}

	// All entities should start as Unknown.
	ctx := context.Background()
	result, err := tracker.TagContext(ctx, agentctx.ContextRequest{UserMessage: ""})
	if err != nil {
		t.Fatalf("GetContext: %v", err)
	}
	if !strings.Contains(result, ": unknown") {
		t.Errorf("expected unknown state in initial context, got:\n%s", result)
	}
}

func TestTracker_Initialize(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
			"person.bob": {
				EntityID:    "person.bob",
				State:       "not_home",
				Attributes:  map[string]any{"friendly_name": "Bob"},
				LastChanged: now.Add(-2 * time.Hour),
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice", "person.bob"}, "UTC", nil)
	err := tracker.Initialize(context.Background(), getter)
	if err != nil {
		t.Fatalf("Initialize: %v", err)
	}

	result, err := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if err != nil {
		t.Fatalf("GetContext: %v", err)
	}

	if !strings.Contains(result, `"name":"Alice"`) {
		t.Errorf("expected Alice in context, got:\n%s", result)
	}
	if !strings.Contains(result, `"state":"home"`) {
		t.Errorf("expected state:home in context, got:\n%s", result)
	}
	if !strings.Contains(result, `"name":"Bob"`) {
		t.Errorf("expected Bob in context, got:\n%s", result)
	}
	if !strings.Contains(result, `"state":"away"`) {
		t.Errorf("expected state:away in context, got:\n%s", result)
	}
}

func TestTracker_Initialize_PartialFailure(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
			// person.bob is missing — will fail.
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice", "person.bob"}, "UTC", nil)
	err := tracker.Initialize(context.Background(), getter)
	if err == nil {
		t.Fatal("expected error for partial failure")
	}

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})

	// Alice should be populated with JSON format.
	if !strings.Contains(result, `"name":"Alice"`) || !strings.Contains(result, `"state":"home"`) {
		t.Errorf("expected Alice with state:home, got:\n%s", result)
	}
	// Bob should show unknown.
	if !strings.Contains(result, "- **Bob**: unknown") {
		t.Errorf("expected Bob: unknown, got:\n%s", result)
	}
}

func TestTracker_HandleStateChange(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	// State changes to not_home.
	tracker.HandleStateChange("person.alice", "home", "not_home")

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if !strings.Contains(result, `"state":"away"`) {
		t.Errorf("expected state:away after state change, got:\n%s", result)
	}
}

func TestTracker_HandleStateChange_IgnoresUntracked(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	// Should not panic or affect tracked entities.
	tracker.HandleStateChange("person.unknown", "home", "not_home")
	tracker.HandleStateChange("light.kitchen", "off", "on")

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if !strings.Contains(result, "- **Alice**: unknown") {
		t.Errorf("expected Alice: unknown unchanged, got:\n%s", result)
	}
}

func TestTracker_HandleStateChange_SameState(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	// Same state — Since should NOT update.
	tracker.HandleStateChange("person.alice", "home", "home")

	tracker.mu.RLock()
	p := tracker.people["person.alice"]
	since := p.Since
	tracker.mu.RUnlock()

	if !since.Equal(now) {
		t.Errorf("expected Since unchanged at %v, got %v", now, since)
	}
}

func TestTracker_HandleStateChange_ClearsRoom(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	// Set a room.
	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if !strings.Contains(result, `"room":"office"`) {
		t.Errorf("expected room:office in JSON, got:\n%s", result)
	}

	// Transition to not_home should clear room.
	tracker.HandleStateChange("person.alice", "home", "not_home")

	result, _ = tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if strings.Contains(result, `"room"`) {
		t.Errorf("expected room cleared after not_home, got:\n%s", result)
	}
}

func TestTracker_GetContext(t *testing.T) {
	chicago, _ := time.LoadLocation("America/Chicago")
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, chicago)

	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
			"person.bob": {
				EntityID:    "person.bob",
				State:       "not_home",
				Attributes:  map[string]any{"friendly_name": "Bob"},
				LastChanged: now.Add(-3 * time.Hour),
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice", "person.bob"}, "America/Chicago", nil)
	_ = tracker.Initialize(context.Background(), getter)

	result, err := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if err != nil {
		t.Fatalf("GetContext: %v", err)
	}

	// Verify heading.
	if !strings.HasPrefix(result, "### People & Presence\n") {
		t.Errorf("expected heading, got:\n%s", result)
	}

	// Verify compact JSON format with delta timestamp.
	if !strings.Contains(result, `"entity":"person.alice"`) || !strings.Contains(result, `"state":"home"`) {
		t.Errorf("expected JSON format with entity and state, got:\n%s", result)
	}
	if !strings.Contains(result, `"since":"-`) {
		t.Errorf("expected delta since field, got:\n%s", result)
	}

	// Verify Alice comes before Bob (insertion order).
	aliceIdx := strings.Index(result, "Alice")
	bobIdx := strings.Index(result, "Bob")
	if aliceIdx < 0 || bobIdx < 0 {
		t.Fatalf("expected both Alice and Bob in output:\n%s", result)
	}
	if aliceIdx > bobIdx {
		t.Errorf("expected Alice before Bob (insertion order), got:\n%s", result)
	}
}

func TestTracker_GetContext_WithRoom(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)
	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})

	// Verify room appears in JSON.
	if !strings.Contains(result, `"room":"office"`) {
		t.Errorf("expected room:office in JSON, got:\n%s", result)
	}
	if !strings.Contains(result, `"room_source":"ap-hor-office"`) {
		t.Errorf("expected room_source in JSON, got:\n%s", result)
	}
}

func TestTracker_GetContext_WithoutRoom(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})

	// No room field should appear when Room is empty (omitempty).
	if strings.Contains(result, `"room"`) {
		t.Errorf("expected no room field for empty room, got:\n%s", result)
	}
}

func TestTracker_GetContext_Empty(t *testing.T) {
	tracker := NewPresenceTracker(nil, "UTC", nil)

	result, err := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if err != nil {
		t.Fatalf("GetContext: %v", err)
	}
	if result != "" {
		t.Errorf("expected empty result for no entities, got %q", result)
	}
}

func TestTracker_GetContext_NotHome(t *testing.T) {
	now := time.Date(2026, 2, 15, 12, 0, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "not_home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	result, _ := tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	if !strings.Contains(result, `"state":"away"`) {
		t.Errorf("expected not_home displayed as state:away, got:\n%s", result)
	}
	if strings.Contains(result, "not_home") {
		t.Errorf("raw not_home should not appear in output:\n%s", result)
	}
}

func TestTracker_UpdateRoom(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	tracker.mu.RLock()
	p := tracker.people["person.alice"]
	room := p.Room
	source := p.RoomSource
	sinceZero := p.RoomSince.IsZero()
	tracker.mu.RUnlock()

	if room != "office" {
		t.Errorf("expected room office, got %q", room)
	}
	if source != "ap-hor-office" {
		t.Errorf("expected source ap-hor-office, got %q", source)
	}
	if sinceZero {
		t.Error("expected RoomSince to be set")
	}
}

func TestTracker_UpdateRoom_SameRoom(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	tracker.mu.RLock()
	firstSince := tracker.people["person.alice"].RoomSince
	tracker.mu.RUnlock()

	// Same room — should not update RoomSince.
	time.Sleep(time.Millisecond)
	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	tracker.mu.RLock()
	secondSince := tracker.people["person.alice"].RoomSince
	tracker.mu.RUnlock()

	if !firstSince.Equal(secondSince) {
		t.Errorf("expected RoomSince unchanged, got %v then %v", firstSince, secondSince)
	}
}

func TestTracker_UpdateRoom_ClearsRoom(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")
	tracker.UpdateRoom("person.alice", "", "")

	tracker.mu.RLock()
	p := tracker.people["person.alice"]
	room := p.Room
	sinceZero := p.RoomSince.IsZero()
	tracker.mu.RUnlock()

	if room != "" {
		t.Errorf("expected empty room, got %q", room)
	}
	if !sinceZero {
		t.Error("expected RoomSince to be zero after clearing")
	}
}

func TestTracker_UpdateRoom_IgnoresUntracked(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	// Should not panic.
	tracker.UpdateRoom("person.unknown", "office", "ap-hor-office")
}

func TestTracker_SetDeviceMACs(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	macs := []string{"aa:bb:cc:dd:ee:ff", "11:22:33:44:55:66"}
	tracker.SetDeviceMACs("person.alice", macs)

	tracker.mu.RLock()
	p := tracker.people["person.alice"]
	got := p.DeviceMACs
	tracker.mu.RUnlock()

	if len(got) != 2 {
		t.Fatalf("expected 2 MACs, got %d", len(got))
	}
	if got[0] != "aa:bb:cc:dd:ee:ff" || got[1] != "11:22:33:44:55:66" {
		t.Errorf("unexpected MACs: %v", got)
	}
}

func TestTracker_SetDeviceMACs_IgnoresUntracked(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)

	// Should not panic.
	tracker.SetDeviceMACs("person.unknown", []string{"aa:bb:cc:dd:ee:ff"})
}

func TestTracker_ConcurrentAccess(t *testing.T) {
	now := time.Date(2026, 2, 15, 16, 30, 0, 0, time.UTC)
	getter := &mockStateGetter{
		states: map[string]*homeassistant.State{
			"person.alice": {
				EntityID:    "person.alice",
				State:       "home",
				Attributes:  map[string]any{"friendly_name": "Alice"},
				LastChanged: now,
			},
		},
	}

	tracker := NewPresenceTracker([]string{"person.alice"}, "UTC", nil)
	_ = tracker.Initialize(context.Background(), getter)

	const goroutines = 20
	const iterations = 100

	var wg sync.WaitGroup
	wg.Add(goroutines * 3)

	// Concurrent state changes.
	for range goroutines {
		go func() {
			defer wg.Done()
			for i := range iterations {
				state := "home"
				if i%2 == 0 {
					state = "not_home"
				}
				tracker.HandleStateChange("person.alice", "", state)
			}
		}()
	}

	// Concurrent context reads.
	for range goroutines {
		go func() {
			defer wg.Done()
			for range iterations {
				_, _ = tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
			}
		}()
	}

	// Concurrent room updates.
	for range goroutines {
		go func() {
			defer wg.Done()
			for i := range iterations {
				room := "office"
				if i%2 == 0 {
					room = "bedroom"
				}
				tracker.UpdateRoom("person.alice", room, "ap-test")
			}
		}()
	}

	wg.Wait()
}

func TestFormatState(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"not_home", "Away"},
		{"NOT_HOME", "Away"},
		{"home", "Home"},
		{"zone.work", "Zone.work"},
		{"Unknown", "Unknown"},
		{"", ""},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := formatState(tt.input)
			if got != tt.want {
				t.Errorf("formatState(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestTitleCase(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"home", "Home"},
		{"away", "Away"},
		{"", ""},
		{"Home", "Home"},
		{"a", "A"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := TitleCase(tt.input)
			if got != tt.want {
				t.Errorf("TitleCase(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestFriendlyNameFromEntityID(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"person.nugget", "nugget"},
		{"person.alice_smith", "alice smith"},
		{"light.kitchen", "kitchen"},
		{"nodomain", "nodomain"},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			got := friendlyNameFromEntityID(tt.input)
			if got != tt.want {
				t.Errorf("friendlyNameFromEntityID(%q) = %q, want %q", tt.input, got, tt.want)
			}
		})
	}
}

func TestTracker_RoomObserver(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "", nil)

	var gotEntity, gotRoom, gotSource string
	var callCount int
	tracker.OnRoomChange(func(entityID, room, source string) {
		gotEntity = entityID
		gotRoom = room
		gotSource = source
		callCount++
	})

	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")

	if callCount != 1 {
		t.Fatalf("observer called %d times, want 1", callCount)
	}
	if gotEntity != "person.alice" {
		t.Errorf("entityID = %q, want %q", gotEntity, "person.alice")
	}
	if gotRoom != "office" {
		t.Errorf("room = %q, want %q", gotRoom, "office")
	}
	if gotSource != "ap-hor-office" {
		t.Errorf("source = %q, want %q", gotSource, "ap-hor-office")
	}
}

func TestTracker_RoomObserverNotCalledOnNoOp(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "", nil)

	var callCount int
	tracker.OnRoomChange(func(_, _, _ string) {
		callCount++
	})

	// Set initial room.
	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")
	if callCount != 1 {
		t.Fatalf("observer called %d times after first update, want 1", callCount)
	}

	// Same room — observer should not fire again.
	tracker.UpdateRoom("person.alice", "office", "ap-hor-office")
	if callCount != 1 {
		t.Errorf("observer called %d times after no-op update, want 1", callCount)
	}

	// Different room — observer should fire.
	tracker.UpdateRoom("person.alice", "bedroom", "ap-hor-bedroom")
	if callCount != 2 {
		t.Errorf("observer called %d times after room change, want 2", callCount)
	}
}

func TestTracker_RoomObserverCalledOutsideLock(t *testing.T) {
	tracker := NewPresenceTracker([]string{"person.alice"}, "", nil)

	// Register an observer that calls GetContext (which takes a read lock).
	// If observers were called under the write lock this would deadlock.
	tracker.OnRoomChange(func(_, _, _ string) {
		_, _ = tracker.TagContext(context.Background(), agentctx.ContextRequest{UserMessage: ""})
	})

	// Should complete without deadlock.
	done := make(chan struct{})
	go func() {
		tracker.UpdateRoom("person.alice", "kitchen", "ap-hor-kitchen")
		close(done)
	}()

	select {
	case <-done:
		// success
	case <-time.After(2 * time.Second):
		t.Fatal("UpdateRoom deadlocked — observer is likely called under the write lock")
	}
}
