package main

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

	"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
	"github.com/urfave/cli/v2"
)

// newTestServiceContext builds a real *cli.Context against serviceFlags (the
// exact flags soundtouch-service registers), so loadConfig tests exercise the
// same parsing/env-var wiring production code does, instead of a hand-rolled
// stand-in that could silently drift from it.
func newTestServiceContext(t *testing.T, args ...string) *cli.Context {
	t.Helper()

	app := &cli.App{Flags: serviceFlags}
	set := flag.NewFlagSet("test", flag.ContinueOnError)

	for _, f := range serviceFlags {
		if err := f.Apply(set); err != nil {
			t.Fatalf("apply flag %v: %v", f.Names(), err)
		}
	}

	if err := set.Parse(args); err != nil {
		t.Fatalf("parse args %v: %v", args, err)
	}

	return cli.NewContext(app, set, nil)
}

func TestResolveFallbackHost(t *testing.T) {
	hostname, _ := os.Hostname()
	if hostname == "" {
		hostname = "localhost"
	}

	hostname = strings.ToLower(hostname)

	cases := []struct {
		name           string
		deploymentMode string
		wantHost       string
		wantWarn       bool
	}{
		{"on-device uses localhost, no warning", "on-device", "localhost", false},
		{"public-network returns no fallback, no warning (caller must fail fast)", "public-network", "", false},
		{"private-network uses this host's own hostname, with warning", "private-network", hostname, true},
		{"unset/legacy behaves like private-network", "", hostname, true},
		{"unrecognized mode behaves like private-network", "some-typo", hostname, true},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			gotHost, gotWarn := resolveFallbackHost(tc.deploymentMode)
			if gotHost != tc.wantHost {
				t.Errorf("host: got %q, want %q", gotHost, tc.wantHost)
			}

			if gotWarn != tc.wantWarn {
				t.Errorf("warnOnUse: got %v, want %v", gotWarn, tc.wantWarn)
			}
		})
	}
}

func TestLoadConfig_DeploymentMode(t *testing.T) {
	t.Run("on-device with no --server-url defaults to localhost", func(t *testing.T) {
		config, err := loadConfig(newTestServiceContext(t, "--deployment-mode=on-device", "--port=8000"))
		if err != nil {
			t.Fatalf("loadConfig: unexpected error: %v", err)
		}

		if config.serverURL != "http://localhost:8000" {
			t.Errorf("serverURL: got %q, want %q", config.serverURL, "http://localhost:8000")
		}

		if config.httpsDefaultURL != "https://localhost:8443" {
			t.Errorf("httpsDefaultURL: got %q, want %q", config.httpsDefaultURL, "https://localhost:8443")
		}
	})

	t.Run("public-network with no --server-url fails fast instead of guessing", func(t *testing.T) {
		_, err := loadConfig(newTestServiceContext(t, "--deployment-mode=public-network"))
		if err == nil {
			t.Fatal("expected an error, got nil")
		}

		if !strings.Contains(err.Error(), "public-network") {
			t.Errorf("expected error to mention public-network, got: %v", err)
		}
	})

	t.Run("public-network with an explicit --server-url succeeds", func(t *testing.T) {
		config, err := loadConfig(newTestServiceContext(t,
			"--deployment-mode=public-network", "--server-url=https://soundtouch.example.com"))
		if err != nil {
			t.Fatalf("loadConfig: unexpected error: %v", err)
		}

		if config.serverURL != "https://soundtouch.example.com" {
			t.Errorf("serverURL: got %q, want %q", config.serverURL, "https://soundtouch.example.com")
		}
	})

	t.Run("unset deployment-mode with no --server-url keeps today's hostname fallback", func(t *testing.T) {
		hostname, _ := os.Hostname()
		if hostname == "" {
			hostname = "localhost"
		}

		hostname = strings.ToLower(hostname)

		config, err := loadConfig(newTestServiceContext(t, "--port=8000"))
		if err != nil {
			t.Fatalf("loadConfig: unexpected error: %v", err)
		}

		want := "http://" + hostname + ":8000"
		if config.serverURL != want {
			t.Errorf("serverURL: got %q, want %q (legacy installs must keep working without --deployment-mode)", config.serverURL, want)
		}
	})

	t.Run("explicit --server-url always wins regardless of deployment-mode", func(t *testing.T) {
		for _, mode := range []string{"", "on-device", "private-network", "public-network"} {
			config, err := loadConfig(newTestServiceContext(t,
				"--deployment-mode="+mode, "--server-url=http://198.51.100.7:8000"))
			if err != nil {
				t.Fatalf("mode %q: loadConfig: unexpected error: %v", mode, err)
			}

			if config.serverURL != "http://198.51.100.7:8000" {
				t.Errorf("mode %q: serverURL: got %q, want explicit override unchanged", mode, config.serverURL)
			}
		}
	})
}

func TestApplyPersistedSettings(t *testing.T) {
	tmpDir, err := os.MkdirTemp("", "main-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	ds := datastore.NewDataStore(tmpDir)

	t.Run("overrides true with false", func(t *testing.T) {
		config := &serviceConfig{
			redact:  true,
			logBody: true,
			record:  true,
		}

		// Simulate the bug by using the old bitwise OR logic in the test,
		// which should fail if we expect false.
		// config.redact = config.redact || false -> stays true

		settings := datastore.Settings{
			RedactLogs:         false,
			LogBodies:          false,
			RecordInteractions: false,
		}
		err := ds.SaveSettings(settings)
		if err != nil {
			t.Fatalf("Failed to save settings: %v", err)
		}

		applyPersistedSettings(ds, config)

		if config.redact != false {
			t.Errorf("Expected redact to be false, got true")
		}
		if config.logBody != false {
			t.Errorf("Expected logBody to be false, got true")
		}
		if config.record != false {
			t.Errorf("Expected record to be false, got true")
		}
	})

	t.Run("retains false when settings are false", func(t *testing.T) {
		settings := datastore.Settings{
			RedactLogs: false,
		}
		err := ds.SaveSettings(settings)
		if err != nil {
			t.Fatalf("Failed to save settings: %v", err)
		}

		config := &serviceConfig{
			redact: false,
		}

		applyPersistedSettings(ds, config)

		if config.redact != false {
			t.Errorf("Expected redact to be false, got true")
		}
	})

	t.Run("overrides false with true", func(t *testing.T) {
		settings := datastore.Settings{
			RedactLogs: true,
		}
		err := ds.SaveSettings(settings)
		if err != nil {
			t.Fatalf("Failed to save settings: %v", err)
		}

		config := &serviceConfig{
			redact: false,
		}

		applyPersistedSettings(ds, config)

		if config.redact != true {
			t.Errorf("Expected redact to be true, got false")
		}
	})
}

func TestMergeTLSExtraHosts(t *testing.T) {
	cases := []struct {
		name      string
		cli       []string
		persisted []string
		want      []string
	}{
		{
			name:      "CLI only",
			cli:       []string{"a.example"},
			persisted: nil,
			want:      []string{"a.example"},
		},
		{
			name:      "Persisted only",
			cli:       nil,
			persisted: []string{"b.example"},
			want:      []string{"b.example"},
		},
		{
			name:      "CLI wins ordering, persisted appended",
			cli:       []string{"a.example"},
			persisted: []string{"b.example"},
			want:      []string{"a.example", "b.example"},
		},
		{
			name:      "Dedupes overlap",
			cli:       []string{"a.example", "b.example"},
			persisted: []string{"b.example", "c.example"},
			want:      []string{"a.example", "b.example", "c.example"},
		},
		{
			name:      "Drops empty + whitespace",
			cli:       []string{"  ", "a.example", ""},
			persisted: []string{"", "  b.example  "},
			want:      []string{"a.example", "b.example"},
		},
		{
			name:      "Both empty",
			cli:       nil,
			persisted: nil,
			want:      []string{},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := mergeTLSExtraHosts(tc.cli, tc.persisted)
			if len(got) != len(tc.want) {
				t.Fatalf("len mismatch: got %v, want %v", got, tc.want)
			}

			for i := range got {
				if got[i] != tc.want[i] {
					t.Errorf("index %d: got %q, want %q (full: %v vs %v)", i, got[i], tc.want[i], got, tc.want)
				}
			}
		})
	}
}

func TestGetDomains_IncludesOAuthDerivation(t *testing.T) {
	// Hostname-based serverURL: the derived OAuth variant must end up
	// in the served TLS cert SAN list, otherwise the speaker rejects
	// the TLS handshake on Spotify / Amazon Music token refresh.
	got := getDomains("http://mac.fritz.box:8000", "https://mac.fritz.box:8443", "mac.fritz.box", nil)

	want := "macoauth.fritz.box"
	if !contains(got, want) {
		t.Errorf("expected SAN list to include %q (derived from serverURL), got: %v", want, got)
	}
}

func TestGetDomains_IPServerURLProducesNoOAuthDerivation(t *testing.T) {
	// IP-based serverURL deliberately yields no derivation (the speaker's
	// `<first-label>oauth.<rest>` construction would be malformed for an
	// IP and no DNS resolver can answer for it). The cert SAN list must
	// not pretend to cover something that can never be queried.
	got := getDomains("http://192.168.0.30:8000", "https://192.168.0.30:8443", "192.168.0.30", nil)

	for _, h := range got {
		if h == "192oauth.168.0.30" {
			t.Errorf("SAN list must not include malformed IP-derived OAuth name, got: %v", got)
		}
	}
}

func contains(haystack []string, needle string) bool {
	for _, h := range haystack {
		if h == needle {
			return true
		}
	}

	return false
}

func TestSettingsFileExists(t *testing.T) {
	dir := t.TempDir()

	if settingsFileExists(dir) {
		t.Fatal("expected false for a dir without settings.json")
	}

	if err := os.WriteFile(filepath.Join(dir, "settings.json"), []byte("{}"), 0o644); err != nil {
		t.Fatalf("write settings.json: %v", err)
	}

	if !settingsFileExists(dir) {
		t.Fatal("expected true once settings.json is present")
	}

	if settingsFileExists("") {
		t.Fatal("expected false for an empty data dir")
	}
}

// applyFirstRunSeed mirrors the startup gate in the CLI Action: a default
// settings.json is written only when none exists yet, so a hand-authored file
// is never clobbered.
func applyFirstRunSeed(ds *datastore.DataStore, config *serviceConfig) {
	existed := settingsFileExists(config.dataDir)

	applyPersistedSettings(ds, config)

	if !existed {
		createDefaultSettings(ds, *config)
	}
}

func TestFirstRunSeed_PreservesHandAuthoredSettings(t *testing.T) {
	dir := t.TempDir()

	// Operator pre-seeds proxy trust but leaves server_url to the --server-url
	// flag. Before the fix this was treated as "first run" and overwritten.
	if err := os.WriteFile(filepath.Join(dir, "settings.json"),
		[]byte(`{"trust_forwarded_headers":true,"trusted_proxy_cidrs":["10.0.0.0/8"]}`), 0o644); err != nil {
		t.Fatalf("write settings.json: %v", err)
	}

	ds := datastore.NewDataStore(dir)
	config := &serviceConfig{dataDir: dir, serverURL: "http://192.0.2.1:8000"}

	applyFirstRunSeed(ds, config)

	got, err := ds.GetSettings()
	if err != nil {
		t.Fatalf("GetSettings: %v", err)
	}

	if !got.TrustForwardedHeaders {
		t.Error("trust_forwarded_headers was clobbered on startup")
	}

	if len(got.TrustedProxyCIDRs) != 1 || got.TrustedProxyCIDRs[0] != "10.0.0.0/8" {
		t.Errorf("trusted_proxy_cidrs was clobbered, got %v", got.TrustedProxyCIDRs)
	}
}

func TestFirstRunSeed_WritesDefaultsWhenAbsent(t *testing.T) {
	dir := t.TempDir()

	ds := datastore.NewDataStore(dir)
	config := &serviceConfig{dataDir: dir, serverURL: "http://192.0.2.1:8000"}

	applyFirstRunSeed(ds, config)

	got, err := ds.GetSettings()
	if err != nil {
		t.Fatalf("GetSettings: %v", err)
	}

	if got.ServerURL != "http://192.0.2.1:8000" {
		t.Errorf("expected defaults to be written with server_url, got %q", got.ServerURL)
	}
}
