package upgrade

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/version"
)

func TestCheckVersionFindsUpdateFromStableAndManifest(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json"}`,
			server.URL)
	})
	mux.HandleFunc("/v1.0.2-rc.1.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json","client":{"install_script":"https://static.pingcap.com/agentteam/client/install.sh","checksums":"https://static.pingcap.com/agentteam/client/v1.0.2-rc.1/agentteam_1.0.2-rc.1_checksums.txt"}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.Equal(t, "v1.0.1", result.Current)
	require.Equal(t, "v1.0.2-rc.1", result.Latest)
	require.True(t, result.HasUpdate)
	require.Equal(t, "https://static.pingcap.com/agentteam/client/install.sh", result.InstallScriptURL)
	require.Equal(t, "https://static.pingcap.com/agentteam/client/v1.0.2-rc.1/agentteam_1.0.2-rc.1_checksums.txt", result.ChecksumsURL)
}

func TestCheckVersionReturnsNoUpdateWhenAlreadyLatest(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	oldVersion := version.Version
	version.Version = "v1.0.2-rc.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json"}`,
			server.URL)
	})
	mux.HandleFunc("/v1.0.2-rc.1.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json","client":{"install_script":"https://static.pingcap.com/agentteam/client/install.sh","checksums":"https://static.pingcap.com/agentteam/client/v1.0.2-rc.1/agentteam_1.0.2-rc.1_checksums.txt"}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.False(t, result.HasUpdate)
	require.Equal(t, "v1.0.2-rc.1", result.Latest)
}

func TestCheckVersionRejectsManifestWithoutChecksums(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json"}`,
			server.URL)
	})
	mux.HandleFunc("/v1.0.2-rc.1.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json","client":{"install_script":"https://static.pingcap.com/agentteam/client/install.sh","checksums":""}}`, server.URL)
	})

	_, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.Error(t, err)
	require.ErrorContains(t, err, "client.checksums")
}

func TestCheckVersionDefaultsInstallScriptWhenManifestOmitsIt(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json"}`,
			server.URL)
	})
	mux.HandleFunc("/v1.0.2-rc.1.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json","client":{"install_script":"","checksums":"https://static.pingcap.com/agentteam/client/v1.0.2-rc.1/agentteam_1.0.2-rc.1_checksums.txt"}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.Equal(t, DefaultInstallScriptURL, result.InstallScriptURL)
}

func TestCheckVersionRejectsUnexpectedProduct(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-client","channel":"stable","current_version":"v1.0.2-rc.1","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2-rc.1.json"}`,
			server.URL)
	})

	_, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.Error(t, err)
	require.ErrorContains(t, err, "is not one of")
}

func TestCheckVersionReturnsManualCommandWhenInstallDirIsNotWritable(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, false)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()

	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json"}`,
			server.URL)
	})
	mux.HandleFunc("/v1.0.2.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json","client":{"checksums":"https://static.pingcap.com/agentteam/client/v1.0.2/agentteam_1.0.2_checksums.txt"}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.Equal(t, UpdateMethodManualCommand, result.UpdateMethod)
	require.False(t, result.CanAutoUpgrade)
	require.Equal(t, VersionInstallCommand("v1.0.2"), result.ManualCommand)
}

func TestCheckVersionRejectsMacOSSelfUpgrade(t *testing.T) {
	setUpgradeTestPlatform(t, "darwin-arm64", false, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()
	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json"}`, server.URL)
	})
	mux.HandleFunc("/v1.0.2.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json","client":{"assets":[{"platform":"darwin-arm64","purpose":"installer","kind":"dmg","url":"https://example.test/AgentTeam.dmg"}]}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.False(t, result.PlatformSupported)
	require.Equal(t, UpdateMethodUnsupported, result.UpdateMethod)
	require.False(t, result.CanAutoUpgrade)
	require.Empty(t, result.RuntimeAssetURL)
	require.Contains(t, result.IneligibilityReason, "not supported")
}

func TestCheckVersionRejectsWindowsSelfUpgrade(t *testing.T) {
	setUpgradeTestPlatform(t, "windows-amd64", false, true)

	oldVersion := version.Version
	version.Version = "v1.0.1"
	defer func() { version.Version = oldVersion }()

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()
	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json"}`, server.URL)
	})
	mux.HandleFunc("/v1.0.2.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json","client":{"assets":[{"platform":"windows-amd64","purpose":"installer","kind":"exe","url":"https://example.test/AgentTeam.exe"}]}}`, server.URL)
	})

	result, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.NoError(t, err)
	require.False(t, result.PlatformSupported)
	require.Equal(t, UpdateMethodUnsupported, result.UpdateMethod)
	require.False(t, result.CanAutoUpgrade)
	require.Empty(t, result.RuntimeAssetURL)
	require.Contains(t, result.IneligibilityReason, "not supported")
}

func TestValidateVersionManifestRejectsInvalidAssetSHA256(t *testing.T) {
	manifest := VersionManifest{
		SchemaVersion: "1",
		Product:       ExpectedProduct,
		Version:       "v1.0.2",
		ManifestURL:   "https://example.test/v1.0.2.json",
		Client: ClientManifest{
			Checksums: "https://example.test/checksums.txt",
			Assets: []ClientAsset{{
				Platform: "linux-amd64",
				Purpose:  clientAssetPurposeRuntime,
				Kind:     clientAssetKindTarball,
				URL:      "https://example.test/agentteam.tar.gz",
				SHA256:   "invalid",
			}},
		},
	}

	err := validateVersionManifest(manifest, "v1.0.2", manifest.Product)
	require.ErrorContains(t, err, "asset[0] has invalid sha256")
}

func TestValidateVersionRejectsUnsafeInput(t *testing.T) {
	require.Error(t, validateVersion(""))
	require.Error(t, validateVersion("1.0.0"))
	require.Error(t, validateVersion("v1.0.0;rm -rf /"))
	require.NoError(t, validateVersion("v1.0.2-rc.1"))
}

func TestCompareVersionsHandlesPrereleaseNumericSegments(t *testing.T) {
	require.Less(t, compareVersions("v1.0.2-rc.2", "v1.0.2-rc.10"), 0)
	require.Greater(t, compareVersions("v1.0.2", "v1.0.2-rc.10"), 0)
	require.Equal(t, 0, compareVersions("v1.0.2+build.1", "v1.0.2+build.9"))
	require.Less(t, compareVersions("v1.0.2-rc.12-67-g75aa8f121", "v1.0.2-rc.15"), 0)
	require.Equal(t, 0, compareVersions("v1.0.2-67-g75aa8f121", "v1.0.2"))
}

func TestCheckVersionDoesNotFallBackToAnotherProductTrack(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()
	mux.HandleFunc("/desktop/stable.json", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})
	mux.HandleFunc("/daemon/stable.json", func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("unexpected daemon fallback request")
	})

	_, err := CheckVersion(context.Background(), server.URL+"/desktop/stable.json")
	require.Error(t, err)
}

func TestCheckVersionFallsBackToLegacyManifestDuringDesktopCutover(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()
	mux.HandleFunc("/desktop/stable.json", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})
	mux.HandleFunc("/daemon/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","channel":"stable","current_version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/daemon/v1.0.2.json"}`, server.URL)
	})
	mux.HandleFunc("/daemon/v1.0.2.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/daemon/v1.0.2.json","client":{"checksums":"https://static.pingcap.com/agentteam/client/v1.0.2/agentteam_1.0.2_checksums.txt"}}`, server.URL)
	})

	result, err := checkVersionFromSources(
		context.Background(),
		[]stableManifestSource{
			{url: server.URL + "/desktop/stable.json", expectedProduct: ExpectedProduct},
			{url: server.URL + "/daemon/stable.json", expectedProduct: LegacyExpectedProduct},
		},
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, "v1.0.2", result.Latest)
}

func TestCheckVersionRejectsVersionManifestFromAnotherProduct(t *testing.T) {
	setUpgradeTestPlatform(t, "linux-amd64", true, true)

	mux := http.NewServeMux()
	server := httptest.NewServer(mux)
	defer server.Close()
	mux.HandleFunc("/stable.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-desktop","channel":"stable","current_version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json"}`, server.URL)
	})
	mux.HandleFunc("/v1.0.2.json", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"schema_version":"1","product":"agentteam-daemon","version":"v1.0.2","published_at":"2026-04-16T04:47:16Z","manifest_url":"%s/v1.0.2.json","client":{"checksums":"https://example.test/checksums.txt"}}`, server.URL)
	})

	_, err := CheckVersion(context.Background(), server.URL+"/stable.json")
	require.ErrorContains(t, err, "does not match stable product")
}

func setUpgradeTestPlatform(t *testing.T, key string, supported bool, writable bool) {
	t.Helper()

	origPlatform := currentPlatform
	origWritable := managedInstallDirWritable
	currentPlatform = func() platformInfo {
		return platformInfo{Key: key, Supported: supported}
	}
	managedInstallDirWritable = func() bool { return writable }
	t.Cleanup(func() {
		currentPlatform = origPlatform
		managedInstallDirWritable = origWritable
	})
}

func TestSupportsHeadlessUpgradeOnlyOnLinux(t *testing.T) {
	require.True(t, SupportsHeadlessUpgrade("linux"))
	require.True(t, SupportsHeadlessUpgrade(" LINUX "))
	require.False(t, SupportsHeadlessUpgrade("darwin"))
	require.False(t, SupportsHeadlessUpgrade("windows"))
	require.False(t, SupportsHeadlessUpgrade(""))
}

func TestPollUntilChecksConditionBeforeWaiting(t *testing.T) {
	calls := 0

	ok := pollUntil(time.Hour, time.Hour, func() bool {
		calls++
		return true
	})

	require.True(t, ok)
	require.Equal(t, 1, calls)
}

func TestPollUntilZeroTimeoutReturnsFalseWhenConditionIsFalse(t *testing.T) {
	calls := 0

	ok := pollUntil(0, time.Hour, func() bool {
		calls++
		return false
	})

	require.False(t, ok)
	require.Equal(t, 1, calls)
}

func TestApplyReleaseRestoresPreviousBinaryWhenInstallerFails(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	fixture.deps.runInstallScript = func(context.Context, string, string, string, string, string, string) error {
		require.NoError(t, os.WriteFile(fixture.managedExe, []byte("new"), 0o755))
		return errors.New("installer failed")
	}

	err := fixture.apply()
	require.ErrorContains(t, err, "installer failed")
	fixture.requireManagedBinary("old")
}

func TestApplyReleaseRejectsNonLinuxPlatform(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	fixture.deps.goos = "darwin"

	err := fixture.apply()
	require.ErrorContains(t, err, "supported only on Linux")
}

func TestApplyReleaseReturnsRollbackErrorWhenInstallerRestoreFails(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	fixture.deps.snapshotManagedBinary = func(targetPath string) (*managedBinaryBackup, error) {
		require.Equal(t, fixture.managedExe, targetPath)
		return &managedBinaryBackup{
			targetPath: targetPath,
			backupPath: filepath.Join(filepath.Dir(targetPath), "missing-backup"),
			existed:    true,
		}, nil
	}
	fixture.deps.runInstallScript = func(context.Context, string, string, string, string, string, string) error {
		require.NoError(t, os.WriteFile(fixture.managedExe, []byte("new"), 0o755))
		return errors.New("installer failed")
	}

	err := fixture.apply()
	require.ErrorContains(t, err, "installer failed")
	require.ErrorContains(t, err, "rollback failed")
	require.ErrorContains(t, err, "stat managed binary backup")
	fixture.requireManagedBinary("new")
}

func TestApplyReleaseRestoresPreviousBinaryWhenRestartHealthFails(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	fixture.writeManagedRuntime("old-runtime")
	fixture.deps.runInstallScript = func(context.Context, string, string, string, string, string, string) error {
		require.NoError(t, os.WriteFile(fixture.managedExe, []byte("new"), 0o755))
		fixture.writeManagedRuntime("new-runtime")
		return nil
	}
	fixture.deps.newServiceController = func(string) DaemonController {
		return &recordingDaemonController{installed: false, running: false}
	}
	fixture.deps.processExists = func(pid int) bool {
		require.Equal(t, 1234, pid)
		return true
	}
	starts := 0
	var stoppedPIDs []int
	fixture.deps.stopProcess = func(pid int) error {
		stoppedPIDs = append(stoppedPIDs, pid)
		return nil
	}
	fixture.deps.startDetachedDaemon = func(path string) (int, error) {
		require.Equal(t, fixture.managedExe, path)
		starts++
		return 9000 + starts, nil
	}
	fixture.deps.waitForDaemonHealthy = func(time.Duration) error { return errors.New("still unhealthy") }

	err := fixture.applyWith(ApplyOptions{DaemonPID: 1234})
	require.ErrorContains(t, err, "health check failed")
	require.Equal(t, 2, starts)
	require.Equal(t, []int{1234, 9001}, stoppedPIDs)
	fixture.requireManagedBinary("old")
	fixture.requireManagedRuntime("old-runtime")
}

func TestApplyReleaseRestartsRestoredDaemonWhenStartFails(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	fixture.deps.newServiceController = func(string) DaemonController {
		return &recordingDaemonController{installed: false, running: false}
	}
	fixture.deps.processExists = func(pid int) bool {
		require.Equal(t, 1234, pid)
		return true
	}
	starts := 0
	fixture.deps.startDetachedDaemon = func(path string) (int, error) {
		require.Equal(t, fixture.managedExe, path)
		starts++
		if starts == 1 {
			return 0, errors.New("new daemon failed")
		}
		return 9000, nil
	}

	err := fixture.applyWith(ApplyOptions{DaemonPID: 1234})
	require.ErrorContains(t, err, "new daemon failed")
	require.Equal(t, 2, starts)
	fixture.requireManagedBinary("old")
}

func TestApplyReleaseStopsManagedServiceBeforeRollbackRestore(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	controller := &recordingDaemonController{installed: true, running: true}
	fixture.deps.newServiceController = func(string) DaemonController {
		return controller
	}
	fixture.deps.restartManagedService = func(string) error {
		return nil
	}
	fixture.deps.waitForDaemonHealthy = func(time.Duration) error { return errors.New("still unhealthy") }

	err := fixture.apply()
	require.ErrorContains(t, err, "managed service health check failed")
	require.Equal(t, []string{"stop", "wait_stop", "start"}, controller.calls)
	fixture.requireManagedBinary("old")
}

func TestApplyReleaseRestartsRestoredServiceWhenRestartFails(t *testing.T) {
	fixture := newApplyReleaseFixture(t)
	controller := &recordingDaemonController{installed: true, running: true}
	fixture.deps.newServiceController = func(string) DaemonController {
		return controller
	}
	fixture.deps.restartManagedService = func(string) error {
		controller.running = false
		return errors.New("restart failed")
	}

	err := fixture.apply()
	require.ErrorContains(t, err, "restart failed")
	require.Equal(t, []string{"start"}, controller.calls)
	fixture.requireManagedBinary("old")
}

type applyReleaseFixture struct {
	t          *testing.T
	managedExe string
	scriptURL  string
	deps       applyReleaseDeps
}

func newApplyReleaseFixture(t *testing.T) *applyReleaseFixture {
	t.Helper()

	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)
	managedExe := filepath.Join(configDir, "bin", "agentteam")
	require.NoError(t, os.MkdirAll(filepath.Dir(managedExe), 0o755))
	require.NoError(t, os.WriteFile(managedExe, []byte("old"), 0o755))

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("#!/usr/bin/env bash\n"))
	}))
	t.Cleanup(server.Close)

	return &applyReleaseFixture{
		t:          t,
		managedExe: managedExe,
		scriptURL:  server.URL,
		deps: applyReleaseDeps{
			goos: "linux",
			runInstallScript: func(context.Context, string, string, string, string, string, string) error {
				return os.WriteFile(managedExe, []byte("new"), 0o755)
			},
			newServiceController:  func(string) DaemonController { return &recordingDaemonController{} },
			snapshotManagedBinary: snapshotManagedBinary,
			restartManagedService: func(string) error { return nil },
			processExists:         func(int) bool { return false },
			stopProcess:           func(int) error { return nil },
			waitForProcessExit:    func(int, time.Duration) error { return nil },
			startDetachedDaemon:   func(string) (int, error) { return 0, nil },
			waitForDaemonHealthy:  func(time.Duration) error { return nil },
		},
	}
}

func (f *applyReleaseFixture) apply() error {
	return f.applyWith(ApplyOptions{})
}

func (f *applyReleaseFixture) applyWith(opts ApplyOptions) error {
	f.t.Helper()
	if opts.Version == "" {
		opts.Version = "v1.0.2"
	}
	if opts.InstallScriptURL == "" {
		opts.InstallScriptURL = f.scriptURL
	}
	return applyRelease(context.Background(), opts, f.deps)
}

func (f *applyReleaseFixture) requireManagedBinary(want string) {
	f.t.Helper()
	data, err := os.ReadFile(f.managedExe)
	require.NoError(f.t, err)
	require.Equal(f.t, want, string(data))
}

func (f *applyReleaseFixture) writeManagedRuntime(content string) {
	f.t.Helper()
	runtimePath := filepath.Join(filepath.Dir(f.managedExe), "runtimes", "opencode", "opencode")
	require.NoError(f.t, os.MkdirAll(filepath.Dir(runtimePath), 0o755))
	require.NoError(f.t, os.WriteFile(runtimePath, []byte(content), 0o755))
}

func (f *applyReleaseFixture) requireManagedRuntime(want string) {
	f.t.Helper()
	runtimePath := filepath.Join(filepath.Dir(f.managedExe), "runtimes", "opencode", "opencode")
	data, err := os.ReadFile(runtimePath)
	require.NoError(f.t, err)
	require.Equal(f.t, want, string(data))
}

type recordingDaemonController struct {
	calls     []string
	installed bool
	running   bool
}

func (r *recordingDaemonController) IsRunning() (bool, int, error) {
	return r.running, 1234, nil
}

func (r *recordingDaemonController) Stop() error {
	r.calls = append(r.calls, "stop")
	r.running = false
	return nil
}

func (r *recordingDaemonController) Start() error {
	r.calls = append(r.calls, "start")
	r.running = true
	return nil
}

func (r *recordingDaemonController) IsInstalled() bool {
	return r.installed
}

func (r *recordingDaemonController) WaitStop(time.Duration) {
	r.calls = append(r.calls, "wait_stop")
}
