package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/config"
)

const haStatesFixture = `[
 {"entity_id":"light.living_room","state":"on","attributes":{"friendly_name":"Гостиная"}},
 {"entity_id":"switch.kettle","state":"off","attributes":{"friendly_name":"Чайник"}},
 {"entity_id":"sensor.bedroom_temp","state":"22.5","attributes":{"friendly_name":"Спальня","unit_of_measurement":"°C"}}
]`

func TestWireSmartHomeOffUnlessEnabled(t *testing.T) {
	st := newTestStore(t)
	for name, cfg := range map[string]*config.Config{
		"no block": {},
		"written but dark": {SmartHome: &config.SmartHomeConfig{
			URL: "http://ha.lan:8123", Token: "t",
		}},
	} {
		t.Run(name, func(t *testing.T) {
			if w := wireSmartHome(cfg, st); w != nil {
				t.Fatal("the house must be off unless the block is enabled")
			}
		})
	}
	// nil wiring must be safe everywhere it is reachable.
	var w *homeWiring
	w.propose(context.Background())
	w.run(context.Background())
	if w.caller() != nil {
		t.Fatal("a nil wiring must have no caller")
	}
	if _, ok := w.homeSummary(context.Background()); ok {
		t.Fatal("a nil wiring must not claim a query")
	}
}

// An unreachable instance must not stop the daemon and must propose nothing.
func TestWireSmartHomeUnreachableIsNotFatal(t *testing.T) {
	st := newTestStore(t)
	w := wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
		// Port 1 on loopback: nothing listens, and it fails fast.
		URL: "http://127.0.0.1:1", Token: "t", Enabled: true,
	}}, st)
	if w == nil {
		t.Fatal("a configured house should still wire")
	}
	tools, err := st.ListTools(context.Background(), "")
	if err != nil {
		t.Fatal(err)
	}
	if len(tools) != 0 {
		t.Fatalf("an instance that never answered must propose nothing, got %+v", tools)
	}
}

// Discovery proposes one row per controllable service, always destructive,
// always 'proposed'. A sensor gets no row: there is nothing to call on it.
func TestProposeOnlyProposesControllableDevices(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(haStatesFixture))
	}))
	defer srv.Close()

	st := newTestStore(t)
	w := wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
		URL: srv.URL, Token: "t", Enabled: true,
	}}, st)
	if w == nil {
		t.Fatal("wireSmartHome returned nil for an enabled, reachable house")
	}
	// Wiring alone must not have touched the house: enumeration happens off
	// the ticker, not on the daemon's start path.
	if pre, err := st.ListTools(context.Background(), ""); err != nil || len(pre) != 0 {
		t.Fatalf("wireSmartHome enumerated the house synchronously: %+v (%v)", pre, err)
	}
	w.propose(context.Background())

	tools, err := st.ListTools(context.Background(), "")
	if err != nil {
		t.Fatal(err)
	}
	got := map[string]bool{}
	for _, tl := range tools {
		got[tl.Name] = true
		if tl.Status != "proposed" {
			t.Errorf("%s status = %q: discovery must never enable", tl.Name, tl.Status)
		}
		if !tl.Destructive {
			t.Errorf("%s is not destructive: every house control needs the confirm turn", tl.Name)
		}
		if len(tl.Cmd) == 0 || tl.Cmd[0] != "smarthome" {
			t.Errorf("%s cmd = %v", tl.Name, tl.Cmd)
		}
	}
	for _, want := range []string{
		"home_light_living_room_on", "home_light_living_room_off",
		"home_switch_kettle_on", "home_switch_kettle_off",
	} {
		if !got[want] {
			t.Errorf("missing proposal %q (have %v)", want, got)
		}
	}
	if len(tools) != 4 {
		t.Fatalf("got %d rows, want 4 — the sensor must not be proposed: %+v", len(tools), tools)
	}

	// A second pass must be idempotent: re-discovery duplicates nothing and
	// never rewrites a row Kami already enabled.
	if err := st.EnableTool(context.Background(), "home_switch_kettle_on",
		[]string{"smarthome", "switch.kettle", "turn_on"}, true, "smarthome:switch", time.Now()); err != nil {
		t.Fatal(err)
	}
	w.propose(context.Background())
	again, err := st.ListTools(context.Background(), "")
	if err != nil {
		t.Fatal(err)
	}
	if len(again) != 4 {
		t.Fatalf("re-discovery duplicated rows: %d", len(again))
	}
	for _, tl := range again {
		if tl.Name == "home_switch_kettle_on" && tl.Status != "enabled" {
			t.Errorf("re-discovery un-enabled a device he had enabled: %q", tl.Status)
		}
	}
}

func TestHomeSummaryReadsState(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(haStatesFixture))
	}))
	defer srv.Close()

	w := wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
		URL: srv.URL, Token: "t", Enabled: true,
	}}, newTestStore(t))
	out, ok := w.homeSummary(context.Background())
	if !ok {
		t.Fatal("summary did not claim the turn")
	}
	if !strings.Contains(out, "Гостиная") {
		t.Errorf("the lamp that is on should be named: %q", out)
	}
	if strings.Contains(out, "Чайник") {
		t.Errorf("a device that is off should not be listed as on: %q", out)
	}
	if !strings.Contains(out, "22.5") {
		t.Errorf("the sensor reading should be there: %q", out)
	}
	// Persona: no masculine self-reference, no "вы", no pet names.
	for _, bad := range []string{"рад ", "готов ", "вы ", "ваш", "милый", "дорогой"} {
		if strings.Contains(strings.ToLower(out), bad) {
			t.Errorf("persona violation %q in %q", bad, out)
		}
	}
}

func TestIsHomeQuery(t *testing.T) {
	yes := []string{
		"что включено дома?",
		"что выключено",
		"какой свет горит дома",
		"свет в доме включен?",
		"какая температура в квартире?",
		"покажи умный дом",
	}
	no := []string{
		"",
		"я дома",
		"буду дома в семь",
		"какая погода дома", // weather wording wins
		"какая температура на улице?",
		"домашние дела", // "дома" must not fire on "домашние"
		"что мне нужно сделать?",
		"напомни выключить чайник в семь", // a reminder, not a house read
	}
	for _, u := range yes {
		if !isHomeQuery(u) {
			t.Errorf("isHomeQuery(%q) = false, want true", u)
		}
	}
	for _, u := range no {
		if isHomeQuery(u) {
			t.Errorf("isHomeQuery(%q) = true, want false", u)
		}
	}
}

// A house that black-holes the connection must not hold the daemon's start.
// wireSmartHome used to enumerate synchronously with a 30s context, inside
// wireVoice, inside run, before the IPC socket was serving — and on the locked
// path, inside the passkey unlock handler.
func TestWireSmartHomeDoesNotBlockOnTheHouse(t *testing.T) {
	// A handler that never answers: the client's own timeout is the only way
	// out, and it is ten seconds.
	block := make(chan struct{})
	defer close(block)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		<-block
	}))
	defer srv.Close()

	done := make(chan *homeWiring, 1)
	go func() {
		done <- wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
			URL: srv.URL, Token: "t", Enabled: true,
		}}, newTestStore(t))
	}()
	select {
	case w := <-done:
		if w == nil {
			t.Fatal("a configured house should still wire")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("wireSmartHome waited on the house")
	}
}

// A lamp that is unreachable is not a lamp that is off, and a list she cut
// short has to say so. Both used to read as plain statements about the house.
func TestHomeSummaryDoesNotCallUnreachableDevicesOff(t *testing.T) {
	const fixture = `[
 {"entity_id":"light.a","state":"unavailable","attributes":{"friendly_name":"Прихожая"}},
 {"entity_id":"light.b","state":"unavailable","attributes":{"friendly_name":"Кухня"}}
]`
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(fixture))
	}))
	defer srv.Close()
	w := wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
		URL: srv.URL, Token: "t", Enabled: true,
	}}, newTestStore(t))
	out, ok := w.homeSummary(context.Background())
	if !ok {
		t.Fatal("summary did not claim the turn")
	}
	if strings.Contains(out, "всё выключено") {
		t.Errorf("two unreachable lamps were reported as off: %q", out)
	}
	if !strings.Contains(out, "не отвечают") {
		t.Errorf("the unreachable devices are not mentioned: %q", out)
	}
}

func TestHomeSummarySaysWhenTheListIsCutShort(t *testing.T) {
	var b strings.Builder
	b.WriteString("[")
	for i := 0; i < 8; i++ {
		if i > 0 {
			b.WriteString(",")
		}
		fmt.Fprintf(&b, `{"entity_id":"light.l%d","state":"on","attributes":{"friendly_name":"лампа%d"}}`, i, i)
	}
	b.WriteString("]")
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(b.String()))
	}))
	defer srv.Close()
	w := wireSmartHome(&config.Config{SmartHome: &config.SmartHomeConfig{
		URL: srv.URL, Token: "t", Enabled: true,
	}}, newTestStore(t))
	out, _ := w.homeSummary(context.Background())
	if !strings.Contains(out, "и ещё 3") {
		t.Errorf("eight lamps on, five named, and nothing said about the rest: %q", out)
	}
}
