package infra

import (
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
	"gopkg.in/yaml.v3"
)

// TestRenderedYAMLParses guards against template-whitespace indentation bugs: every rendered
// compose/config/static-config must be well-formed YAML across the token matrix.
func TestRenderedYAMLParses(t *testing.T) {
	for i, s := range allRendered(t) {
		var v any
		if err := yaml.Unmarshal([]byte(s), &v); err != nil {
			t.Fatalf("rendered output #%d is not valid YAML: %v\n---\n%s", i, err, s)
		}
	}
}

// allComposeStrings renders every compose/config we emit, across the token/token-less matrix, so a
// single ":latest" anywhere is caught.
func allRendered(t *testing.T) []string {
	t.Helper()
	var out []string
	for _, td := range []TraefikData{
		{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"},
		{ACMEEmail: "admin@example.com"}, // email, no CF token → HTTP-01
		{},                               // token-less / LAN-only
	} {
		files, err := RenderTraefik(td)
		if err != nil {
			t.Fatalf("RenderTraefik(%+v): %v", td, err)
		}
		for _, f := range files {
			out = append(out, f.Content)
		}
	}
	cf, err := RenderCloudflared(CloudflaredData{CFTunnelToken: "tunnel-tok"})
	if err != nil {
		t.Fatalf("RenderCloudflared: %v", err)
	}
	for _, f := range cf {
		out = append(out, f.Content)
	}
	out = append(out, RenderFileBrowserCompose("example.com", nil))
	out = append(out, RenderFileBrowserCompose("example.com", []string{"      - /mnt/hdd_1:/srv/hdd_1"}))
	out = append(out, RenderFileBrowserConfig(nil))
	out = append(out, RenderServersTransports())
	return out
}

func TestServersTransports(t *testing.T) {
	s := RenderServersTransports()
	// The named transport must be defined under http.serversTransports with insecureSkipVerify, so a
	// docker-provider service can reference it as `<name>@file`.
	if !strings.Contains(s, "serversTransports:") || !strings.Contains(s, ServersTransportInsecure+":") {
		t.Errorf("named serversTransport %q missing:\n%s", ServersTransportInsecure, s)
	}
	if !strings.Contains(s, "insecureSkipVerify: true") {
		t.Errorf("insecureSkipVerify not set on the named transport:\n%s", s)
	}
	var v any
	if err := yaml.Unmarshal([]byte(s), &v); err != nil {
		t.Fatalf("serversTransports config is not valid YAML: %v\n%s", err, s)
	}
}

func TestNoLatestTagSurvives(t *testing.T) {
	for _, c := range []string{TraefikImage, CloudflaredImage, FileBrowserImage} {
		if strings.HasSuffix(c, ":latest") || !strings.Contains(c, ":") {
			t.Fatalf("image constant is not pinned: %q", c)
		}
	}
	for _, s := range allRendered(t) {
		if strings.Contains(s, ":latest") {
			t.Fatalf(":latest survived in rendered output:\n%s", s)
		}
	}
}

func TestTraefikWithCloudflareToken(t *testing.T) {
	files, err := RenderTraefik(TraefikData{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"})
	if err != nil {
		t.Fatal(err)
	}
	yml := files["traefik.yml"].Content
	if !strings.Contains(yml, "certResolver: letsencrypt") {
		t.Error("expected certResolver on websecure when ACME email set")
	}
	if !strings.Contains(yml, "dnsChallenge") || !strings.Contains(yml, "provider: cloudflare") {
		t.Error("expected Cloudflare DNS-01 challenge when CF API token set")
	}
	// The wildcard is NOT in traefik.yml — the entrypoint-level domains doesn't trigger issuance.
	if strings.Contains(yml, "domains:") {
		t.Error("traefik.yml must not carry the entrypoint domains block (proven not to issue)")
	}
	if strings.Contains(yml, "httpChallenge") {
		t.Error("HTTP-01 must NOT appear when a CF API token is set")
	}
	if !strings.Contains(yml, "email: admin@example.com") {
		t.Error("ACME email must appear in the cert-resolver block")
	}

	compose := files["docker-compose.yml"].Content
	if !strings.Contains(compose, "env_file") {
		t.Error("expected env_file in traefik compose when CF API token set")
	}
	if !strings.Contains(compose, TraefikImage) {
		t.Errorf("expected pinned traefik image %q in compose", TraefikImage)
	}

	env, ok := files[".env"]
	if !ok {
		t.Fatal("expected a .env file when CF API token is set")
	}
	if !strings.Contains(env.Content, "CF_DNS_API_TOKEN=cf-api-tok") {
		t.Errorf("CF API token not wired into .env: %q", env.Content)
	}
	if env.Mode != 0o600 {
		t.Errorf(".env must be 0600 (carries the CF token), got %o", env.Mode)
	}
	if files["traefik.yml"].Mode != 0o644 || files["docker-compose.yml"].Mode != 0o644 {
		t.Error("traefik.yml and docker-compose.yml must be 0644")
	}
}

func TestTraefikEmailNoCloudflareToken(t *testing.T) {
	files, err := RenderTraefik(TraefikData{ACMEEmail: "admin@example.com"})
	if err != nil {
		t.Fatal(err)
	}
	yml := files["traefik.yml"].Content
	if !strings.Contains(yml, "httpChallenge") {
		t.Error("expected HTTP-01 challenge when email set but no CF token")
	}
	if strings.Contains(yml, "dnsChallenge") {
		t.Error("DNS-01 must NOT appear without a CF token")
	}
	if _, ok := files[".env"]; ok {
		t.Error("no .env should be emitted without a CF API token")
	}
}

func TestTraefikTokenless(t *testing.T) {
	files, err := RenderTraefik(TraefikData{})
	if err != nil {
		t.Fatal(err)
	}
	yml := files["traefik.yml"].Content
	if strings.Contains(yml, "certificatesResolvers") || strings.Contains(yml, "certResolver") {
		t.Error("token-less node must emit no cert resolver (traefik serves self-signed)")
	}
	compose := files["docker-compose.yml"].Content
	if strings.Contains(compose, "env_file") {
		t.Error("token-less compose must not reference env_file")
	}
	if _, ok := files[".env"]; ok {
		t.Error("token-less node must emit no .env")
	}
	// Structural difference vs the with-token case is the whole point: the resolver section is absent.
	withTok, _ := RenderTraefik(TraefikData{ACMEEmail: "admin@example.com", CFAPIToken: "x"})
	if withTok["traefik.yml"].Content == yml {
		t.Error("token-less and with-token traefik.yml must differ structurally")
	}
}

func TestCloudflaredRender(t *testing.T) {
	files, err := RenderCloudflared(CloudflaredData{CFTunnelToken: "tunnel-tok-123"})
	if err != nil {
		t.Fatal(err)
	}
	compose := files["docker-compose.yml"].Content
	if !strings.Contains(compose, "TUNNEL_TOKEN=tunnel-tok-123") {
		t.Errorf("tunnel token not wired into cloudflared env: %q", compose)
	}
	if !strings.Contains(compose, CloudflaredImage) {
		t.Errorf("expected pinned cloudflared image %q", CloudflaredImage)
	}
	if !strings.Contains(compose, "command: tunnel run") {
		t.Error("expected `command: tunnel run`")
	}
}

func TestControllerRoute(t *testing.T) {
	// Wildcard path (DNS-01 ACME): the route anchors *.<domain> + apex proactive issuance.
	r := RenderControllerRoute("demo-felhom.eu", true)
	if !strings.Contains(r, "Host(`felhom.demo-felhom.eu`)") {
		t.Errorf("domain not wired into controller route rule: %q", r)
	}
	if !strings.Contains(r, "http://felhom-controller:8080") {
		t.Errorf("controller service URL missing: %q", r)
	}
	if !strings.Contains(r, "websecure") {
		t.Error("controller route must be on the websecure entrypoint")
	}
	if !strings.Contains(r, "certResolver: letsencrypt") ||
		!strings.Contains(r, `main: "*.demo-felhom.eu"`) || !strings.Contains(r, `- "demo-felhom.eu"`) {
		t.Errorf("wildcard issuance anchor missing on the DNS-01 controller route:\n%s", r)
	}
	var v any
	if err := yaml.Unmarshal([]byte(r), &v); err != nil {
		t.Fatalf("controller route (wildcard) is not valid YAML: %v\n%s", err, r)
	}

	// Non-ACME path: plain TLS, no resolver/domains, still valid YAML.
	plain := RenderControllerRoute("demo-felhom.eu", false)
	if strings.Contains(plain, "certResolver") || strings.Contains(plain, "domains:") {
		t.Errorf("non-ACME route must not carry certResolver/domains:\n%s", plain)
	}
	if err := yaml.Unmarshal([]byte(plain), &v); err != nil {
		t.Fatalf("controller route (plain) is not valid YAML: %v\n%s", err, plain)
	}
}

func TestFileBrowserRender(t *testing.T) {
	compose := RenderFileBrowserCompose("demo-felhom.eu", nil)
	if !strings.Contains(compose, "Host(`files.demo-felhom.eu`)") {
		t.Errorf("domain not wired into filebrowser routing label: %q", compose)
	}
	if !strings.Contains(compose, FileBrowserImage) {
		t.Errorf("expected pinned filebrowser image %q", FileBrowserImage)
	}

	// Default config (no storage paths) → a single /srv source.
	def := RenderFileBrowserConfig(nil)
	if !strings.Contains(def, `- path: "/srv"`) {
		t.Errorf("empty config must default to a /srv source: %q", def)
	}

	// With paths → a named per-drive source.
	withPaths := RenderFileBrowserConfig([]settings.StoragePath{{Path: "/mnt/hdd_1", Label: "Media"}})
	if !strings.Contains(withPaths, `- path: "/srv/hdd_1"`) || !strings.Contains(withPaths, `name: "Media"`) {
		t.Errorf("storage path not wired into filebrowser config: %q", withPaths)
	}

	// Storage mounts wire into the compose volumes section.
	withMounts := RenderFileBrowserCompose("demo-felhom.eu", []string{"      - /mnt/hdd_1:/srv/hdd_1"})
	if !strings.Contains(withMounts, "/mnt/hdd_1:/srv/hdd_1") {
		t.Errorf("storage mount not wired into filebrowser compose: %q", withMounts)
	}

	// v0.66.2: the entrypoint wrapper sets umask 002 so customer-created folders are group-writable
	// (2775). The image doesn't honor a UMASK env, so this MUST be the entrypoint mechanism.
	if !strings.Contains(compose, `entrypoint: ["sh", "-c", "umask 002; exec /home/filebrowser/filebrowser"]`) {
		t.Errorf("filebrowser compose missing the umask-002 entrypoint wrapper: %q", compose)
	}
}
