package storage

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

// F12 (CAMPAIGN-3, CRITICAL): NEITHER rendered network-storage unit may carry a network-online.target
// ordering — that is exactly what closed the boot ordering cycle. The .mount keeps `_netdev` (the
// correct, sufficient network ordering for the real mount). Companion red-proof: re-adding either
// `After=`/`Wants=network-online.target` line to a template makes these assertions fail.
func TestRenderNetworkUnits_NoNetworkOnlineOrdering(t *testing.T) {
	for _, spec := range []NetworkMountSpec{
		{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000},
		{Name: "docs", Protocol: ProtocolSMB, Server: "10.0.0.6", Export: "share", CredsRef: "/var/lib/felhom-agent/smb-creds/docs.cred", MappedUID: 1000, MappedGID: 1000},
	} {
		mountUnit := renderNetworkMountUnit(spec)
		autoUnit := renderNetworkAutomountUnit(spec)
		if strings.Contains(mountUnit, "network-online.target") {
			t.Errorf("[%s] .mount unit still orders network-online.target (F12 cycle):\n%s", spec.Name, mountUnit)
		}
		if strings.Contains(autoUnit, "network-online.target") {
			t.Errorf("[%s] .automount unit still orders network-online.target (F12 cycle):\n%s", spec.Name, autoUnit)
		}
		if !strings.Contains(mountUnit, "_netdev") {
			t.Errorf("[%s] .mount unit lost _netdev — the ONLY correct network ordering for the real mount:\n%s", spec.Name, mountUnit)
		}
	}
}

// specFromNetworkUnits must reconstruct a spec that re-renders EXACTLY the installed unit pair — the
// idempotency contract of the drift reconcile. A round-trip that isn't byte-exact would make
// MigrateNetworkUnits rewrite on every pass (never converging).
func TestSpecFromNetworkUnits_RoundTrips(t *testing.T) {
	for _, spec := range []NetworkMountSpec{
		{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000, IdleTimeoutSec: 60},
		{Name: "docs", Protocol: ProtocolSMB, Server: "10.0.0.6", Export: "share", CredsRef: "/var/lib/felhom-agent/smb-creds/docs.cred", MappedUID: 1000, MappedGID: 1000, IdleTimeoutSec: 120},
	} {
		mountUnit := renderNetworkMountUnit(spec)
		autoUnit := renderNetworkAutomountUnit(spec)
		got, ok := specFromNetworkUnits(mountUnit, autoUnit)
		if !ok {
			t.Fatalf("[%s] specFromNetworkUnits failed to parse its own render", spec.Name)
		}
		if renderNetworkMountUnit(got) != mountUnit {
			t.Errorf("[%s] .mount round-trip mismatch:\nWANT:\n%s\nGOT:\n%s", spec.Name, mountUnit, renderNetworkMountUnit(got))
		}
		if renderNetworkAutomountUnit(got) != autoUnit {
			t.Errorf("[%s] .automount round-trip mismatch:\nWANT:\n%s\nGOT:\n%s", spec.Name, autoUnit, renderNetworkAutomountUnit(got))
		}
	}
}

// legacyNetworkUnitPair renders the pre-0.85 units WITH the F12 network-online ordering, the exact
// drift the migration must repair.
func legacyNetworkUnitPair(spec NetworkMountSpec) (mount, auto string) {
	mount = renderNetworkMountUnit(spec)
	mount = strings.Replace(mount, "\n[Mount]\n", "\nAfter=network-online.target\nWants=network-online.target\n[Mount]\n", 1)
	auto = renderNetworkAutomountUnit(spec)
	auto = strings.Replace(auto, "\n[Automount]\n", "\nAfter=network-online.target\nWants=network-online.target\n[Automount]\n", 1)
	return mount, auto
}

// MigrateNetworkUnits rewrites a drifted (legacy, network-online-carrying) unit pair exactly ONCE,
// batches a single daemon-reload, and is idempotent (a second pass rewrites nothing). A clean unit is
// left untouched.
func TestMigrateNetworkUnits_RewritesDriftedOnceIdempotent(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("systemd-escaped unit filename contains a backslash; exercised on the Linux build server")
	}
	unitDir := t.TempDir()
	stageDir := t.TempDir()
	spec := NetworkMountSpec{Name: "media", Protocol: ProtocolNFS, Server: "10.0.0.5", Export: "/srv/media", MappedUID: 1000, MappedGID: 1000, IdleTimeoutSec: 60}
	mountName, err := UnitNameForMount(spec.Where())
	if err != nil {
		t.Fatalf("unit name: %v", err)
	}
	autoName := strings.TrimSuffix(mountName, ".mount") + ".automount"
	legacyMount, legacyAuto := legacyNetworkUnitPair(spec)
	writeFile(t, filepath.Join(unitDir, mountName), legacyMount)
	writeFile(t, filepath.Join(unitDir, autoName), legacyAuto)

	rr := &recordingRunner{}
	// installUnit stages then `install`s (recorded, not executed); rewrite the unit dir copy ourselves
	// so the on-disk content reflects the migration for the idempotency re-read.
	ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: stageDir, Host: &fakeHostReader{}, Logger: quietLogger()})

	migrated := ops.MigrateNetworkUnits(context.Background())
	if migrated != 1 {
		t.Fatalf("first pass must migrate exactly 1 unit, got %d", migrated)
	}
	// The recorded calls must include exactly one daemon-reload (batched), plus the two installs.
	reloads, installs := 0, 0
	for _, c := range rr.calls {
		joined := strings.Join(c, " ")
		if strings.Contains(joined, "daemon-reload") {
			reloads++
		}
		if strings.Contains(joined, "install") {
			installs++
		}
	}
	if reloads != 1 {
		t.Errorf("migration must batch exactly ONE daemon-reload, got %d (calls: %v)", reloads, rr.calls)
	}
	if installs != 2 {
		t.Errorf("migration must rewrite both units (2 installs), got %d", installs)
	}
	// The staged content the installer would have placed must be the CLEAN template. Simulate the
	// install landing (installUnit staged to stageDir/<unit>), then re-read for idempotency.
	applyStaged(t, stageDir, unitDir, mountName)
	applyStaged(t, stageDir, unitDir, autoName)
	if got := readFile(t, filepath.Join(unitDir, mountName)); strings.Contains(got, "network-online.target") {
		t.Errorf("migrated .mount still carries network-online.target:\n%s", got)
	}

	rr.calls = nil
	if migrated := ops.MigrateNetworkUnits(context.Background()); migrated != 0 {
		t.Fatalf("second pass over already-current units must migrate 0, got %d", migrated)
	}
	if len(rr.calls) != 0 {
		t.Errorf("idempotent second pass must construct ZERO commands, got: %v", rr.calls)
	}
}

// A non-marker unit file in the unit dir is never touched by the migration.
func TestMigrateNetworkUnits_IgnoresForeignUnits(t *testing.T) {
	unitDir := t.TempDir()
	writeFile(t, filepath.Join(unitDir, "some-service.mount"), "[Unit]\nDescription=not ours\n[Mount]\nWhere=/x\n")
	rr := &recordingRunner{}
	ops := NewSudoHostOps(SudoHostOpsConfig{Runner: rr, Bins: Binaries{}.withDefaults(), UnitDir: unitDir, StageDir: t.TempDir(), Host: &fakeHostReader{}, Logger: quietLogger()})
	if migrated := ops.MigrateNetworkUnits(context.Background()); migrated != 0 {
		t.Fatalf("a foreign unit must not be migrated, got %d", migrated)
	}
	if len(rr.calls) != 0 {
		t.Errorf("a foreign unit must construct zero commands, got: %v", rr.calls)
	}
}

func writeFile(t *testing.T, path, content string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func readFile(t *testing.T, path string) string {
	t.Helper()
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	return string(b)
}

// applyStaged mirrors what the (recorded, not executed) `install` would do: copy the agent-staged unit
// into the unit dir, so the idempotency re-read sees the migrated content.
func applyStaged(t *testing.T, stageDir, unitDir, unitName string) {
	t.Helper()
	src := filepath.Join(stageDir, unitName)
	b, err := os.ReadFile(src)
	if err != nil {
		return // installUnit stages before the recorded install; if absent, nothing to mirror
	}
	writeFile(t, filepath.Join(unitDir, unitName), string(b))
}
