package config

import (
	"encoding/json"
	"testing"
	"time"
)

// Absent blocks must read as off on a nil receiver: the daemon calls these
// helpers before it knows whether the operator configured anything.
func TestSensesOffByDefault(t *testing.T) {
	var cfg Config
	if cfg.Media.StoreDir() != "" {
		t.Error("media store dir is set with no media block")
	}
	if cfg.Vision.LooksAtImages() {
		t.Error("vision is on with no vision block")
	}
	if cfg.Capture.Records() {
		t.Error("the recorder is on with no capture block")
	}
	if cfg.Capture.MaxDuration() != 0 {
		t.Error("a nil capture block invented a duration")
	}
	if cfg.Speaker.Recognizes() {
		t.Error("speaker recognition is on with no speaker block")
	}
}

// The recorder is the capability that most needs its default to be off, so it
// gets its own test rather than a line in the one above.
func TestCaptureIsOffUntilExplicitlyEnabled(t *testing.T) {
	cases := []struct {
		name string
		c    *CaptureConfig
		want bool
	}{
		{"absent", nil, false},
		{"present but not enabled", &CaptureConfig{MaxMinutes: 60}, false},
		{"enabled", &CaptureConfig{Enabled: true}, true},
	}
	for _, c := range cases {
		if got := c.c.Records(); got != c.want {
			t.Errorf("%s: Records() = %v, want %v", c.name, got, c.want)
		}
	}
}

func TestCaptureBlockParsesFromJSON(t *testing.T) {
	raw := `{"capture":{"enabled":true,"max_minutes":45,"stt_window":"2m",
	  "chunk_runes":2000,"max_chunks":10,"save_transcript":true}}`
	var cfg Config
	if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if !cfg.Capture.Records() {
		t.Fatal("capture did not parse as enabled")
	}
	if cfg.Capture.MaxDuration() != 45*time.Minute {
		t.Errorf("max duration = %v", cfg.Capture.MaxDuration())
	}
	if time.Duration(cfg.Capture.STTWindow) != 2*time.Minute {
		t.Errorf("stt window = %v", time.Duration(cfg.Capture.STTWindow))
	}
	if cfg.Capture.ChunkRunes != 2000 || cfg.Capture.MaxChunks != 10 {
		t.Errorf("summariser limits = %+v", cfg.Capture)
	}
	if !cfg.Capture.SaveTranscript {
		t.Error("save_transcript did not parse")
	}
}

// Keeping the verbatim record of what other people said is the heavier act, so
// it is separately opt-in from recording at all.
func TestTranscriptIsNotSavedByDefault(t *testing.T) {
	var cfg Config
	if err := json.Unmarshal([]byte(`{"capture":{"enabled":true}}`), &cfg); err != nil {
		t.Fatal(err)
	}
	if cfg.Capture.SaveTranscript {
		t.Error("transcripts are saved without anyone asking")
	}
	if cfg.Capture.MaxDuration() != 0 {
		t.Error("max_minutes defaulted in config instead of in the package")
	}
}

// enabled with nothing to talk to is a misconfiguration, not a capability.
func TestVisionNeedsBothEnabledAndEndpoint(t *testing.T) {
	cases := []struct {
		name string
		v    *VisionConfig
		want bool
	}{
		{"absent", nil, false},
		{"endpoint but not enabled", &VisionConfig{Endpoint: "http://127.0.0.1:8081"}, false},
		{"enabled but no endpoint", &VisionConfig{Enabled: true}, false},
		{"enabled, blank endpoint", &VisionConfig{Enabled: true, Endpoint: "   "}, false},
		{"both", &VisionConfig{Enabled: true, Endpoint: "http://127.0.0.1:8081"}, true},
	}
	for _, c := range cases {
		if got := c.v.LooksAtImages(); got != c.want {
			t.Errorf("%s: LooksAtImages() = %v, want %v", c.name, got, c.want)
		}
	}
}

func TestSensesBlocksParseFromJSON(t *testing.T) {
	raw := `{
	  "db_path": "/tmp/x.db",
	  "socket_path": "/tmp/x.sock",
	  "media": {"dir": "media", "retention": "48h", "max_bytes": 1048576},
	  "vision": {
	    "enabled": true,
	    "endpoint": "http://127.0.0.1:8081",
	    "model": "qwen2.5-vl",
	    "max_dim": 640,
	    "max_tokens": 200,
	    "timeout": "45s",
	    "prompt": "Что тут?"
	  }
	}`
	var cfg Config
	if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if cfg.Media.StoreDir() != "media" {
		t.Errorf("media dir = %q", cfg.Media.StoreDir())
	}
	if time.Duration(cfg.Media.Retention) != 48*time.Hour {
		t.Errorf("retention = %v", time.Duration(cfg.Media.Retention))
	}
	if cfg.Media.MaxBytes != 1<<20 {
		t.Errorf("max_bytes = %d", cfg.Media.MaxBytes)
	}
	if !cfg.Vision.LooksAtImages() {
		t.Fatal("vision did not parse as enabled")
	}
	if cfg.Vision.MaxDim != 640 || cfg.Vision.MaxTokens != 200 {
		t.Errorf("vision limits = %+v", cfg.Vision)
	}
	if time.Duration(cfg.Vision.Timeout) != 45*time.Second {
		t.Errorf("vision timeout = %v", time.Duration(cfg.Vision.Timeout))
	}
	if cfg.Vision.Prompt != "Что тут?" {
		t.Errorf("prompt = %q", cfg.Vision.Prompt)
	}
}

// A media dir set with no vision block is a valid state, and the useful one on a
// box with no vision model: images can be kept, they just cannot be described.
func TestMediaWithoutVisionIsValid(t *testing.T) {
	var cfg Config
	if err := json.Unmarshal([]byte(`{"media":{"dir":"/srv/media"}}`), &cfg); err != nil {
		t.Fatal(err)
	}
	if cfg.Media.StoreDir() != "/srv/media" {
		t.Errorf("dir = %q", cfg.Media.StoreDir())
	}
	if cfg.Vision.LooksAtImages() {
		t.Error("vision came on by itself")
	}
}

// A voiceprint is a biometric of a named person. Nothing about it turns on by
// itself: no speaker block means no recognition, and no enrolment either.
func TestSpeakerIsOffUntilExplicitlyEnabled(t *testing.T) {
	var cfg Config
	if err := json.Unmarshal([]byte(`{}`), &cfg); err != nil {
		t.Fatal(err)
	}
	if cfg.Speaker.Recognizes() {
		t.Error("speaker recognition came on with no config at all")
	}
	var empty Config
	if err := json.Unmarshal([]byte(`{"speaker":{}}`), &empty); err != nil {
		t.Fatal(err)
	}
	if empty.Speaker.Recognizes() {
		t.Error("an empty speaker block enabled recognition")
	}
}

// Enabled alone is not enough: recognition needs a model, and on this box there
// is none. Recognizes() must stay false so the daemon reports the honest state
// instead of claiming a capability it cannot perform.
func TestSpeakerNeedsBothEnabledAndAModel(t *testing.T) {
	var cfg Config
	if err := json.Unmarshal([]byte(`{"speaker":{"enabled":true}}`), &cfg); err != nil {
		t.Fatal(err)
	}
	if cfg.Speaker.Recognizes() {
		t.Error("enabled with no model_path claimed to recognise")
	}
	var only Config
	if err := json.Unmarshal([]byte(`{"speaker":{"model_path":"/opt/x.onnx"}}`), &only); err != nil {
		t.Fatal(err)
	}
	if only.Speaker.Recognizes() {
		t.Error("a model_path alone enabled recognition")
	}
}

func TestSpeakerBlockParsesFromJSON(t *testing.T) {
	const raw = `{"speaker":{"enabled":true,"model_path":"/opt/maven/models/spk/ecapa.onnx",` +
		`"lib_path":"/opt/maven/lib","threshold":0.62,"min_seconds":1.5}}`
	var cfg Config
	if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if !cfg.Speaker.Recognizes() {
		t.Fatal("speaker did not parse as enabled")
	}
	if cfg.Speaker.ModelPath != "/opt/maven/models/spk/ecapa.onnx" {
		t.Errorf("model_path = %q", cfg.Speaker.ModelPath)
	}
	if cfg.Speaker.LibPath != "/opt/maven/lib" {
		t.Errorf("lib_path = %q", cfg.Speaker.LibPath)
	}
	if cfg.Speaker.Threshold != 0.62 || cfg.Speaker.MinSeconds != 1.5 {
		t.Errorf("thresholds = %+v", cfg.Speaker)
	}
}

// A typo in the vision endpoint, or a media block with no dir, used to be
// logged once at wiring time and the capability just stayed off. A capability
// that silently does not exist is the hardest misconfiguration to notice, so
// both fail at startup now.
func TestSensesBlocksAreValidatedAtStartup(t *testing.T) {
	bad := map[string]string{
		"media with no dir":        `{"media":{"retention":"48h"}}`,
		"negative budget":          `{"media":{"dir":"/srv/media","max_total_bytes":-1}}`,
		"blob over the budget":     `{"media":{"dir":"/srv/media","max_bytes":100,"max_total_bytes":10}}`,
		"vision with no media dir": `{"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
		"vision endpoint typo":     `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"127.0.0.1:8081"}}`,
		"vision on the wan":        `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://8.8.8.8:8081"}}`,
		"vision, empty endpoint":   `{"media":{"dir":"/srv/media"},"vision":{"enabled":true}}`,
		"capture with no store":    `{"capture":{"enabled":true}}`,
	}
	for name, body := range bad {
		t.Run(name, func(t *testing.T) {
			if _, err := Load(writeConfig(t, body)); err == nil {
				t.Fatal("want a startup error")
			}
		})
	}
	good := map[string]string{
		"media alone":     `{"media":{"dir":"/srv/media"}}`,
		"media + vision":  `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
		"media + capture": `{"media":{"dir":"/srv/media"},"capture":{"enabled":true}}`,
		"vision off":      `{"vision":{"endpoint":"http://8.8.8.8:8081"}}`,
	}
	for name, body := range good {
		t.Run(name, func(t *testing.T) {
			if _, err := Load(writeConfig(t, body)); err != nil {
				t.Fatalf("valid config refused: %v", err)
			}
		})
	}
}
