package config

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

// TestDeployConfigLoads parses the file the box actually runs on.
//
// Every other test in this package builds its own JSON, so a key renamed in one
// place and not the other would go unnoticed until the daemon refused to start.
// This one reads deploy/mavend.json through the same Load the daemon calls, so
// a config change and a code change have to agree here or the suite is red.
//
// The ${VAR} expansions come from a gitignored deploy/telegram.env that is not
// present in CI. Live blocks reject empty secrets; this test supplies inert
// Telegram values and verifies that every credential-less block is explicitly
// disabled.
func TestDeployConfigLoads(t *testing.T) {
	path := filepath.Join("..", "..", "deploy", "mavend.json")
	if _, err := os.Stat(path); err != nil {
		t.Skipf("no deploy config at %s: %v", path, err)
	}
	// Live blocks fail on expanded-empty credentials. CI provides inert values
	// so this test exercises the committed shape; explicitly disabled blocks
	// (ntfy, workstation and Home Assistant) require none.
	t.Setenv("TELEGRAM_BOT_TOKEN", "test-token")
	t.Setenv("TELEGRAM_CHAT_ID", "-1001234567890")
	t.Setenv("MAVEN_STT_TOKEN", "test-token")
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load(%s): %v", path, err)
	}

	// Spot-check the settings whose absence would be a silent behaviour change
	// rather than a startup error.
	if cfg.Phraser == nil {
		t.Fatal("deploy config has no phraser block")
	}
	if cfg.Phraser.NGpuLayers == 0 {
		t.Error("phraser.n_gpu_layers is 0 — llama-server would run CPU-only, " +
			"because nothing in this package defaults that field")
	}
	if cfg.Voice == nil || !cfg.Voice.Enabled {
		t.Fatal("deploy config does not enable voice")
	}
	if !cfg.Voice.UseLLMRouter() {
		t.Error("deploy config turned the LLM router off")
	}
	if cfg.Voice.RouterThreshold <= 0 {
		t.Error("router threshold did not get its default")
	}
	// The recall thresholds are a measured pair. An explicit deployment value
	// silently overriding a retuned default would make the eval and the box run
	// different safety gates, so pin both directions here.
	if cfg.Voice.QueryMinScore != DefaultQueryMinScore || cfg.Voice.QueryMinMargin != DefaultQueryMinMargin {
		t.Errorf("deploy recall gate is %.3f/%.3f, defaults are %.3f/%.3f",
			cfg.Voice.QueryMinScore, cfg.Voice.QueryMinMargin,
			DefaultQueryMinScore, DefaultQueryMinMargin)
	}

	// The second reach (V-649). The token is a ${VAR} that CI cannot resolve, so
	// the committed deployment makes the dark state explicit. Removing disabled
	// without provisioning a credential makes runtime wiring fail startup; it
	// can never silently publish anonymously.
	if cfg.Ntfy == nil {
		t.Fatal("deploy config has no ntfy block — sev3-away and away reminders " +
			"would have nowhere to land, and would vanish silently rather than fail")
	}
	if cfg.Ntfy.BaseURL == "" || cfg.Ntfy.Topic == "" {
		t.Errorf("ntfy block is incomplete: base_url=%q topic=%q", cfg.Ntfy.BaseURL, cfg.Ntfy.Topic)
	}
	if !cfg.Ntfy.Disabled {
		t.Fatal("deploy ntfy reach has no checked-in credential and must remain explicitly disabled")
	}
	if cfg.Workstation == nil || !cfg.Workstation.ModelDisabled || cfg.Workstation.Stt == nil {
		t.Fatalf("deploy must disable only its uncredentialed model arm and retain authenticated STT: %+v", cfg.Workstation)
	}
}

func TestCanonicalDeployEnvExampleNamesEverySecret(t *testing.T) {
	path := filepath.Join("..", "..", "deploy", "telegram.env.example")
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	example := string(b)
	references := map[string]bool{
		// These two live outside ${...}: DBKeyEnv names an environment variable
		// as JSON data, while the separately deployed Python child reads its own
		// environment directly.
		"CW2_TOKEN":    true,
		"MAVEN_DB_KEY": true,
	}
	for _, source := range []string{
		filepath.Join("..", "..", "deploy", "mavend.json"),
		filepath.Join("..", "..", "docker-compose.yml"),
	} {
		raw, err := os.ReadFile(source)
		if err != nil {
			t.Fatalf("read deploy source %s: %v", source, err)
		}
		_ = os.Expand(string(raw), func(expr string) string {
			// Compose supports ${NAME:-default}; os.Expand deliberately hands the
			// full braced expression to this callback.
			name, _, _ := strings.Cut(expr, ":-")
			references[name] = true
			return ""
		})
	}
	for name := range references {
		if !strings.Contains(example, name+"=") {
			t.Errorf("canonical deploy env example omits %s", name)
		}
	}
}

// Go's flag package treats a bare boolean flag as true and does not consume a
// following "true"/"false" argument. That following value becomes the first
// positional argument and stops parsing, so every flag after it silently keeps
// its default. Keep the Compose boolean in -name=value form (V-691 live QA).
func TestComposeAmbientBooleanDoesNotStopFlagParsing(t *testing.T) {
	path := filepath.Join("..", "..", "docker-compose.yml")
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	compose := string(b)
	const joined = `"-ambient-enabled=${MAVEN_AMBIENT_ENABLED:-false}"`
	if !strings.Contains(compose, joined) {
		t.Fatalf("mavweb ambient boolean must be one argv element %s", joined)
	}
	if strings.Contains(compose, `"-ambient-enabled",`) {
		t.Fatal("bare -ambient-enabled leaves its value positional and prevents -ambient-token from being parsed")
	}
}
