package updater

import (
	"archive/zip"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestDoUpgrade(t *testing.T) {
	tmpDir := t.TempDir()
	BundlePath = filepath.Join(tmpDir, "Ollama.app")
	appContents := filepath.Join(BundlePath, "Contents")
	appBackupDir = filepath.Join(tmpDir, "backup")
	appContentsOld := filepath.Join(appBackupDir, "Ollama.app", "Contents")
	UpdateStageDir = filepath.Join(tmpDir, "updates")
	UpgradeMarkerFile = filepath.Join(tmpDir, "upgraded")
	bundle := filepath.Join(UpdateStageDir, "foo", "ollama-darwin.zip")

	err := os.MkdirAll(filepath.Join(appContents, "MacOS"), 0o755)
	if err != nil {
		t.Fatal("failed to create empty dirs")
	}
	err = os.MkdirAll(filepath.Join(BundlePath, "Contents", "Resources"), 0o755)
	if err != nil {
		t.Fatal("failed to create empty dirs")
	}
	err = os.MkdirAll(filepath.Dir(bundle), 0o755)
	if err != nil {
		t.Fatal("failed to create empty dirs")
	}

	// No update file, simple failure scenario
	if err := DoUpgrade(false); err == nil {
		t.Fatal("expected failure without download")
	} else if !strings.Contains(err.Error(), "failed to lookup downloads") {
		t.Fatalf("unexpected error: %s", err.Error())
	}

	// Start with an unreadable zip file
	if err := os.WriteFile(bundle, []byte{0x4b, 0x50, 0x40, 0x03, 0x00, 0x0a, 0x00}, 0o755); err != nil {
		t.Fatalf("failed to create intentionally corrupt zip file: %s", err)
	}
	if err := DoUpgrade(false); err == nil {
		t.Fatal("expected failure with corrupt zip file")
	} else if !strings.Contains(err.Error(), "unable to open upgrade bundle") {
		t.Fatalf("unexpected error with corrupt zip file: %s", err)
	}

	// Generate valid (partial) zip file for remaining scenarios
	if err := zipCreationHelper(bundle, []testPayload{
		{
			Name: "Ollama.app/Contents/MacOS/Ollama",
			Body: []byte("would be app binary"),
		},
		{
			Name: "Ollama.app/Contents/Resources/ollama",
			Body: []byte("would be the cli"),
		},
		{
			Name: "Ollama.app/Contents/Resources/dummy",
			Body: []byte("./ollama"),
			Mode: os.ModeSymlink,
		},
	}); err != nil {
		t.Fatal(err)
	}
	// Permission failure on rename
	if err := os.Chmod(BundlePath, 0o500); err != nil {
		t.Fatal("failed to remove write permission")
	}
	if err := DoUpgrade(false); err == nil {
		t.Fatal("expected failure with no permission to rename Contents")
	} else if !strings.Contains(err.Error(), "permission problems") {
		t.Fatalf("unexpected error with permission failure: %s", err)
	}
	if err := os.Chmod(BundlePath, 0o755); err != nil {
		t.Fatal("failed to restore write permission")
	}

	// Prior failed upgrade
	if err := os.MkdirAll(appContentsOld, 0o755); err != nil {
		t.Fatal("failed to create empty dirs")
	}
	if err := DoUpgrade(false); err == nil {
		t.Fatal("expected failure with old contents existing")
	} else if !strings.Contains(err.Error(), "prior upgrade failed") {
		t.Fatalf("unexpected error with old contents: %s", err)
	}
	if err := os.RemoveAll(appBackupDir); err != nil {
		t.Fatal("failed to cleanup dir")
	}

	// TODO - a failure mode where we revert the backup

	// Happy path
	if err := DoUpgrade(false); err != nil {
		t.Fatalf("unexpected error with clean setup: %s", err)
	}
	if _, err := os.Stat(appContentsOld); err != nil {
		t.Fatalf("missing %s", appContentsOld)
	}
	if _, err := os.Stat(UpgradeMarkerFile); err != nil {
		t.Fatalf("missing marker %s", UpgradeMarkerFile)
	}
	if _, err := os.Stat(filepath.Join(BundlePath, "Contents", "MacOS", "Ollama")); err != nil {
		t.Fatalf("missing new App")
	}
	if _, err := os.Stat(filepath.Join(BundlePath, "Contents", "Resources", "ollama")); err != nil {
		t.Fatalf("missing new cli")
	}

	// Cleanup before next attempt
	if err := DoPostUpgradeCleanup(); err != nil {
		t.Fatal("failed to cleanup dir")
	}

	err = os.MkdirAll(filepath.Dir(bundle), 0o755)
	if err != nil {
		t.Fatal("failed to create empty dirs")
	}

	// Zip file with one corrupt file within to trigger a rollback
	if err := os.WriteFile(bundle, corruptZipData, 0o755); err != nil {
		t.Fatalf("failed to create intentionally corrupt zip file: %s", err)
	}
	if err := DoUpgrade(false); err == nil {
		t.Fatal("expected failure with corrupt zip file")
	} else if !strings.Contains(err.Error(), "failed to open bundle file") {
		t.Fatalf("unexpected error with corrupt zip file: %s", err)
	}
	// Make sure things were restored on partial failure
	if _, err := os.Stat(appContents); err != nil {
		t.Fatalf("missing %s", appContents)
	}
	if _, err := os.Stat(appContentsOld); err == nil {
		t.Fatal("old contents still exists")
	}
	if _, err := os.Stat(filepath.Join(BundlePath, "Contents", "MacOS", "Ollama")); err != nil {
		t.Fatalf("missing old App")
	}
	if _, err := os.Stat(filepath.Join(BundlePath, "Contents", "Resources", "ollama")); err != nil {
		t.Fatalf("missing old cli")
	}
}

func TestDoUpgradeAtStartup(t *testing.T) {
	tmpDir := t.TempDir()
	BundlePath = filepath.Join(tmpDir, "Ollama.app")
	appBackupDir = filepath.Join(tmpDir, "backup")
	UpdateStageDir = filepath.Join(tmpDir, "updates")
	UpgradeMarkerFile = filepath.Join(tmpDir, "upgraded")
	bundle := filepath.Join(UpdateStageDir, "foo", "ollama-darwin.zip")

	if err := DoUpgradeAtStartup(); err == nil {
		t.Fatal("expected failure without download")
	} else if !strings.Contains(err.Error(), "failed to lookup downloads") {
		t.Fatalf("unexpected error: %s", err.Error())
	}

	if err := os.MkdirAll(filepath.Dir(bundle), 0o755); err != nil {
		t.Fatal("failed to create empty dirs")
	}

	if err := zipCreationHelper(bundle, []testPayload{
		{
			Name: "Ollama.app/Contents/MacOS/Ollama",
			Body: []byte("would be app binary"),
		},
		{
			Name: "Ollama.app/Contents/Resources/ollama",
			Body: []byte("would be the cli"),
		},
		{
			Name: "Ollama.app/Contents/Resources/dummy",
			Body: []byte("./ollama"),
			Mode: os.ModeSymlink,
		},
	}); err != nil {
		t.Fatal(err)
	}

	if err := DoUpgradeAtStartup(); err != nil {
		t.Fatalf("unexpected error with verification failure: %s", err)
	}
	if _, err := os.Stat(bundle); err == nil {
		t.Fatalf("unverified bundle still exists %s", bundle)
	}
}

func TestVerifyDownloadFailures(t *testing.T) {
	tmpDir := t.TempDir()
	BundlePath = filepath.Join(tmpDir, "Ollama.app")
	UpdateStageDir = filepath.Join(tmpDir, "staging")
	bundle := filepath.Join(UpdateStageDir, "foo", "ollama-darwin.zip")
	if err := os.MkdirAll(filepath.Dir(bundle), 0o755); err != nil {
		t.Fatal("failed to create empty dirs")
	}
	tests := []struct {
		n        string
		in       []testPayload
		expected string
	}{
		{"breakout", []testPayload{
			{
				Name: "Ollama.app/",
				Body: []byte{},
			}, {
				Name: "Ollama.app/Resources/ollama",
				Body: []byte("cli payload here"),
			}, {
				Name: "Ollama.app/Contents/MacOS/Ollama",
				Body: []byte("../../../../breakout"),
				Mode: os.ModeSymlink,
			},
		}, "bundle contains link outside"},
		{"absolute", []testPayload{{
			Name: "Ollama.app/Contents/MacOS/Ollama",
			Body: []byte("/etc/foo"),
			Mode: os.ModeSymlink,
		}}, "bundle contains absolute"},
		{"missing", []testPayload{{
			Name: "Ollama.app/Contents/MacOS/Ollama",
			Body: []byte("../nothere"),
			Mode: os.ModeSymlink,
		}}, "no such file or directory"},
		{"unsigned", []testPayload{{
			Name: "Ollama.app/Contents/MacOS/Ollama",
			Body: []byte{0xfa, 0xcf, 0xfe, 0xed, 0x00, 0x0c, 0x01, 0x00},
		}}, "signature verification failed"},
	}

	for _, tt := range tests {
		t.Run(tt.n, func(t *testing.T) {
			_ = os.Remove(bundle)
			if err := zipCreationHelper(bundle, tt.in); err != nil {
				t.Fatal(err)
			}
			err := VerifyDownload()
			if err == nil || !strings.Contains(err.Error(), tt.expected) {
				t.Fatalf("expected \"%s\" got %s", tt.expected, err)
			}
		})
	}
}

// One file has been corrupted to cause a checksum mismatch
var corruptZipData = []byte{0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xed, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x55, 0x54, 0x9, 0x0, 0x3, 0x6d, 0x6c, 0x5f, 0x67, 0x6e, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd8, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x55, 0x54, 0x9, 0x0, 0x3, 0x48, 0x6c, 0x5f, 0x67, 0x58, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe3, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x4d, 0x61, 0x63, 0x4f, 0x53, 0x2f, 0x55, 0x54, 0x9, 0x0, 0x3, 0x59, 0x6c, 0x5f, 0x67, 0x9f, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe3, 0x7e, 0x8f, 0x59, 0xe3, 0x6, 0x15, 0x70, 0x14, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x20, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x4d, 0x61, 0x63, 0x4f, 0x53, 0x2f, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x55, 0x54, 0x9, 0x0, 0x3, 0x59, 0x6c, 0x5f, 0x67, 0x83, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x43, 0x4f, 0x52, 0x52, 0x55, 0x50, 0x54, 0xa, 0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x55, 0x54, 0x9, 0x0, 0x3, 0x66, 0x6c, 0x5f, 0x67, 0x83, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x3, 0x4, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x7e, 0x8f, 0x59, 0x19, 0xa5, 0x62, 0xf7, 0x11, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1c, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x6f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x55, 0x54, 0x9, 0x0, 0x3, 0x66, 0x6c, 0x5f, 0x67, 0x66, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x77, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0xa, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xed, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0xed, 0x41, 0x0, 0x0, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x55, 0x54, 0x5, 0x0, 0x3, 0x6d, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd8, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x14, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0xed, 0x41, 0x45, 0x0, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x55, 0x54, 0x5, 0x0, 0x3, 0x48, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe3, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0xed, 0x41, 0x93, 0x0, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x4d, 0x61, 0x63, 0x4f, 0x53, 0x2f, 0x55, 0x54, 0x5, 0x0, 0x3, 0x59, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe3, 0x7e, 0x8f, 0x59, 0xe3, 0x6, 0x15, 0x70, 0x14, 0x0, 0x0, 0x0, 0x14, 0x0, 0x0, 0x0, 0x20, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa4, 0x81, 0xe7, 0x0, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x4d, 0x61, 0x63, 0x4f, 0x53, 0x2f, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x55, 0x54, 0x5, 0x0, 0x3, 0x59, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x7e, 0x8f, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0xed, 0x41, 0x55, 0x1, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x55, 0x54, 0x5, 0x0, 0x3, 0x66, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x1, 0x2, 0x1e, 0x3, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x7e, 0x8f, 0x59, 0x19, 0xa5, 0x62, 0xf7, 0x11, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x24, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa4, 0x81, 0xad, 0x1, 0x0, 0x0, 0x4f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x2e, 0x61, 0x70, 0x70, 0x2f, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x6f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x55, 0x54, 0x5, 0x0, 0x3, 0x66, 0x6c, 0x5f, 0x67, 0x75, 0x78, 0xb, 0x0, 0x1, 0x4, 0xf5, 0x1, 0x0, 0x0, 0x4, 0x14, 0x0, 0x0, 0x0, 0x50, 0x4b, 0x5, 0x6, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x6, 0x0, 0x3f, 0x2, 0x0, 0x0, 0x1c, 0x2, 0x0, 0x0, 0x0, 0x0}

type testPayload struct {
	Name string
	Body []byte
	Mode fs.FileMode
}

func zipCreationHelper(filename string, files []testPayload) error {
	fd, err := os.Create(filename)
	if err != nil {
		return err
	}

	w := zip.NewWriter(fd)
	for _, file := range files {
		fh := &zip.FileHeader{
			Name:  file.Name,
			Flags: 0,
		}
		if file.Mode != 0 {
			fh.SetMode(file.Mode)
		}
		f, err := w.CreateHeader(fh)
		if err != nil {
			return err
		}
		_, err = f.Write(file.Body)
		if err != nil {
			return err
		}
	}
	return w.Close()
}

func TestAlreadyMoved(t *testing.T) {
	oldPath := SystemWidePath
	defer func() {
		SystemWidePath = oldPath
	}()
	exe, err := os.Executable()
	if err != nil {
		t.Fatal("failed to find executable path")
	}
	tmpDir := t.TempDir()
	testApp := filepath.Join(tmpDir, "Ollama.app")
	err = os.MkdirAll(filepath.Join(testApp, "Contents", "MacOS"), 0o755)
	if err != nil {
		t.Fatal("failed to create Contents dir")
	}
	SystemWidePath = testApp
	testBinary := filepath.Join(testApp, "Contents", "MacOS", "Ollama")
	if err := os.Symlink(exe, testBinary); err != nil {
		t.Fatalf("failed to create symlink to executable: %s", err)
	}

	bundle := alreadyMoved()
	if bundle != testApp {
		t.Fatalf("expected %s, got %s", testApp, bundle)
	}

	// "Keep scenario"
	testApp = filepath.Join(tmpDir, "Ollama 2.app")
	err = os.MkdirAll(filepath.Join(testApp, "Contents", "MacOS"), 0o755)
	if err != nil {
		t.Fatal("failed to create Contents dir")
	}
	testBinary = filepath.Join(testApp, "Contents", "MacOS", "Ollama")
	if err := os.Symlink(exe, testBinary); err != nil {
		t.Fatalf("failed to create symlink to executable: %s", err)
	}

	bundle = alreadyMoved()
	if bundle != testApp {
		t.Fatalf("expected %s, got %s", testApp, bundle)
	}
}
