// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: LicenseRef-MutuaL-1.2

package appconfig

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

	"git.secluded.site/cooked-mcp/internal/configvalue"
)

func TestLoadACIDAuthenticationCredentials21ResolvesTOMLConfigValues(t *testing.T) {
	t.Setenv("COOKED_USERNAME_FROM_MY_SHELL", "shell-user")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "$COOKED_USERNAME_FROM_MY_SHELL"
password = "!printf shell-password"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.Username != "shell-user" {
		t.Fatalf("Username = %q, want shell-user", got.Username)
	}
	if got.Password != "shell-password" {
		t.Fatalf("Password = %q, want shell-password", got.Password)
	}
}

func TestLoadACIDAuthenticationCredentials11EnvironmentOverridesTOML(t *testing.T) {
	t.Setenv("COOKED_LOGIN_USERNAME", "override-user")
	t.Setenv("COOKED_LOGIN_PASSWORD", "override-password")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.Username != "override-user" {
		t.Fatalf("Username = %q, want override-user", got.Username)
	}
	if got.Password != "override-password" {
		t.Fatalf("Password = %q, want override-password", got.Password)
	}
}

func TestLoadDefaultsHTTPAddr(t *testing.T) {
	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPAddr != "127.0.0.1:8123" {
		t.Fatalf("HTTPAddr = %q, want default loopback address", got.HTTPAddr)
	}
}

// server.CONFIG.8-2
func TestLoadACIDServerConfig8_2LoadsTOMLHTTPAddr(t *testing.T) {
	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_addr = "127.0.0.1:9000"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPAddr != "127.0.0.1:9000" {
		t.Fatalf("HTTPAddr = %q, want TOML address", got.HTTPAddr)
	}
}

// server.CONFIG.3 server.CONFIG.8-1
func TestLoadACIDServerConfig3And8_1EnvironmentOverridesTOMLHTTPAddr(t *testing.T) {
	t.Setenv("COOKED_MCP_HTTP_ADDR", "127.0.0.1:9001")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_addr = "127.0.0.1:9000"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPAddr != "127.0.0.1:9001" {
		t.Fatalf("HTTPAddr = %q, want environment address", got.HTTPAddr)
	}
}

// server.CONFIG.8
func TestLoadACIDServerConfig8HTTPAddrOverridesEnvironmentAndTOML(t *testing.T) {
	t.Setenv("COOKED_MCP_HTTP_ADDR", "127.0.0.1:9001")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_addr = "127.0.0.1:9000"
`)

	got, err := Load(
		context.Background(),
		configPath,
		configvalue.NewResolver(),
		LoadOptions{HTTPAddr: "127.0.0.1:9002"},
	)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPAddr != "127.0.0.1:9002" {
		t.Fatalf("HTTPAddr = %q, want explicit address", got.HTTPAddr)
	}
}

// server.SECURITY.4 server.SECURITY.9
func TestLoadACIDServerSecurity4And9EnvironmentOverridesTOMLHTTPToken(t *testing.T) {
	t.Setenv("COOKED_MCP_HTTP_TOKEN", "env-token")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_token = "toml-token"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPToken != "env-token" {
		t.Fatalf("HTTPToken = %q, want environment token", got.HTTPToken)
	}
}

// server.SECURITY.5 server.SECURITY.10 server.SECURITY.12 server.SECURITY.13
func TestLoadACIDServerSecurity5And10ResolvesTOMLHTTPTokenConfigValue(t *testing.T) {
	t.Setenv("TEST_HTTP_TOKEN", "shell-token")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_token = "$TEST_HTTP_TOKEN"
`)

	got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}

	if got.HTTPToken != "shell-token" {
		t.Fatalf("HTTPToken = %q, want resolved TOML token", got.HTTPToken)
	}
}

// server.SECURITY.11
func TestLoadACIDServerSecurity11RejectsEmptyEnvironmentHTTPToken(t *testing.T) {
	t.Setenv("COOKED_MCP_HTTP_TOKEN", "")

	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"
`)

	_, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err == nil {
		t.Fatal("Load() error = nil, want empty HTTP token error")
	}
	if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
		t.Fatalf("Load() error = %v, want empty HTTP token error", err)
	}
}

// server.SECURITY.11
func TestLoadACIDServerSecurity11RejectsEmptyTOMLHTTPToken(t *testing.T) {
	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_token = ""
`)

	_, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err == nil {
		t.Fatal("Load() error = nil, want empty HTTP token error")
	}
	if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
		t.Fatalf("Load() error = %v, want empty HTTP token error", err)
	}
}

// server.SECURITY.1-1
func TestLoadACIDServerSecurity1_1AllowsLoopbackHTTPAddrWithoutToken(t *testing.T) {
	tests := []struct {
		name string
		addr string
	}{
		{name: "ipv4", addr: "127.0.0.1:8123"},
		{name: "localhost", addr: "localhost:8123"},
		{name: "ipv6", addr: "[::1]:8123"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configPath := filepath.Join(t.TempDir(), "config.toml")
			writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_addr = "`+tt.addr+`"
`)

			got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
			if err != nil {
				t.Fatalf("Load() error = %v", err)
			}
			if got.HTTPAddr != tt.addr {
				t.Fatalf("HTTPAddr = %q, want %q", got.HTTPAddr, tt.addr)
			}
			if got.HTTPToken != "" {
				t.Fatalf("HTTPToken = %q, want empty", got.HTTPToken)
			}
		})
	}
}

// server.SECURITY.1-1
func TestLoadACIDServerSecurity1_1AllowsHTTPAddrWithoutToken(t *testing.T) {
	tests := []struct {
		name string
		addr string
	}{
		{name: "all interfaces", addr: "0.0.0.0:8123"},
		{name: "LAN", addr: "192.168.1.10:8123"},
		{name: "Tailscale", addr: "100.64.0.1:8123"},
		{name: "hostname-like LAN", addr: "192.168.1.blah:8123"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configPath := filepath.Join(t.TempDir(), "config.toml")
			writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[mcp]
http_addr = "`+tt.addr+`"
`)

			got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
			if err != nil {
				t.Fatalf("Load() error = %v", err)
			}
			if got.HTTPAddr != tt.addr {
				t.Fatalf("HTTPAddr = %q, want %q", got.HTTPAddr, tt.addr)
			}
			if got.HTTPToken != "" {
				t.Fatalf("HTTPToken = %q, want empty", got.HTTPToken)
			}
		})
	}
}

// server.SECURITY.1-1
func TestLoadACIDServerSecurity1_1AllowsHTTPAddrOverrideWithoutToken(t *testing.T) {
	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"
`)

	got, err := Load(
		context.Background(),
		configPath,
		configvalue.NewResolver(),
		LoadOptions{HTTPAddr: "0.0.0.0:8123"},
	)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if got.HTTPAddr != "0.0.0.0:8123" {
		t.Fatalf("HTTPAddr = %q, want override", got.HTTPAddr)
	}
	if got.HTTPToken != "" {
		t.Fatalf("HTTPToken = %q, want empty", got.HTTPToken)
	}
}

func TestLoadRejectsMissingExplicitConfigFile(t *testing.T) {
	_, err := Load(
		context.Background(),
		filepath.Join(t.TempDir(), "missing.toml"),
		configvalue.NewResolver(),
		LoadOptions{},
	)
	if err == nil {
		t.Fatal("Load() error = nil, want missing config error")
	}
}

func TestLoadACIDServerConfig72RejectsMissingDefaultConfigHome(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", "")

	originalUserHomeDir := userHomeDir
	userHomeDir = func() (string, error) {
		return "", os.ErrNotExist
	}
	t.Cleanup(func() {
		userHomeDir = originalUserHomeDir
	})

	_, err := Load(context.Background(), "", configvalue.NewResolver(), LoadOptions{})
	if err == nil {
		t.Fatal("Load() error = nil, want default config path error")
	}
	if !strings.Contains(err.Error(), "resolve default config path") {
		t.Fatalf("Load() error = %v, want default config path error", err)
	}
}

func TestLoadRejectsNonLocalHTTPBaseURL(t *testing.T) {
	configPath := filepath.Join(t.TempDir(), "config.toml")
	writeConfig(t, configPath, `[user]
name = "toml-user"
password = "toml-password"

[cooked]
base_url = "http://example.com"
`)

	_, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
	if err == nil {
		t.Fatal("Load() error = nil, want non-local HTTP base URL error")
	}
	if !strings.Contains(err.Error(), "must use https") {
		t.Fatalf("Load() error = %v, want https error", err)
	}
}

func writeConfig(t *testing.T, path, content string) {
	t.Helper()

	if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
}
