package diff

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

	"github.com/blacktop/ipsw/internal/search"
	"github.com/blacktop/ipsw/pkg/info"
	"github.com/blacktop/ipsw/pkg/ota/types"
	"github.com/blacktop/ipsw/pkg/plist"
)

func writeDiffTestFile(t *testing.T, path, content string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("MkdirAll(%q) error = %v", filepath.Dir(path), err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("WriteFile(%q) error = %v", path, err)
	}
}

func TestUnsupportedFlagsForOTAMode(t *testing.T) {
	tests := []struct {
		name     string
		conf     Config
		expected []string
	}{
		{
			name:     "no flags",
			conf:     Config{},
			expected: nil,
		},
		{
			name: "now-supported flags not blocked",
			conf: Config{
				LaunchD:      true,
				Firmware:     true,
				Features:     true,
				Entitlements: true,
				Files:        true,
				Sandbox:      true,
				CStrings:     true,
			},
			expected: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := unsupportedFlagsForOTAMode(&tt.conf)
			if len(got) != len(tt.expected) {
				t.Fatalf(
					"got %v, want %v", got, tt.expected,
				)
			}
			for i := range got {
				if got[i] != tt.expected[i] {
					t.Errorf(
						"got[%d] = %q, want %q",
						i, got[i], tt.expected[i],
					)
				}
			}
		})
	}
}

func TestUnsupportedFlagsForDirectoryMode(t *testing.T) {
	tests := []struct {
		name     string
		conf     Config
		expected []string
	}{
		{
			name:     "no flags",
			conf:     Config{},
			expected: nil,
		},
		{
			name:     "launchd blocked",
			conf:     Config{LaunchD: true},
			expected: []string{"--launchd"},
		},
		{
			name:     "sandbox blocked",
			conf:     Config{Sandbox: true},
			expected: []string{"--sandbox"},
		},
		{
			name: "supported flags not blocked",
			conf: Config{
				Files:        true,
				Features:     true,
				Entitlements: true,
			},
			expected: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := unsupportedFlagsForDirectoryMode(&tt.conf)
			if len(got) != len(tt.expected) {
				t.Fatalf("got %v, want %v", got, tt.expected)
			}
			for i := range got {
				if got[i] != tt.expected[i] {
					t.Errorf("got[%d] = %q, want %q", i, got[i], tt.expected[i])
				}
			}
		})
	}
}

func TestLaunchdConfigFromRootsPrefersSbinLaunchd(t *testing.T) {
	tmpDir := t.TempDir()
	sbinDir := filepath.Join(tmpDir, "sbin")
	if err := os.MkdirAll(sbinDir, 0o755); err != nil {
		t.Fatalf("MkdirAll() error = %v", err)
	}

	launchdPath := filepath.Join(sbinDir, "launchd")
	if err := os.WriteFile(launchdPath, []byte("not-a-macho"), 0o644); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}

	_, err := launchdConfigFromRoots([]string{tmpDir})
	if err == nil {
		t.Fatal("expected error for invalid launchd binary")
	}
	if !strings.Contains(err.Error(), launchdPath) && !strings.Contains(err.Error(), "launchd") {
		t.Fatalf("error = %q, want mention of launchd path", err)
	}
}

func TestOTALaunchdSearchRootsIncludesPayloadAndMounts(t *testing.T) {
	ctx := &Context{
		payloadRoot: "/tmp/payload",
		Mount: map[string]mount{
			"SystemOS": {MountPath: "/tmp/system"},
			"AppOS":    {MountPath: "/tmp/app"},
		},
	}

	got := otaLaunchdSearchRoots(ctx)
	want := []string{"/tmp/payload", "/tmp/app", "/tmp/system"}
	if len(got) != len(want) {
		t.Fatalf("roots = %v, want %v", got, want)
	}
	for i := range got {
		if got[i] != want[i] {
			t.Fatalf("roots[%d] = %q, want %q", i, got[i], want[i])
		}
	}
}

func TestValidateOTAScope(t *testing.T) {
	tests := []struct {
		name    string
		info    *info.Info
		wantErr bool
		errMsg  string
	}{
		{
			name: "full OTA passes",
			info: &info.Info{
				Plists: &plist.Plists{
					OTAInfo: &plist.OTAInfo{
						MobileAssetProperties: types.Asset{},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "nil OTAInfo passes",
			info: &info.Info{
				Plists: &plist.Plists{},
			},
			wantErr: false,
		},
		{
			name: "partial OTA rejected",
			info: &info.Info{
				Plists: &plist.Plists{
					OTAInfo: &plist.OTAInfo{
						MobileAssetProperties: types.Asset{
							PrerequisiteBuild: "22A5282m",
						},
					},
				},
			},
			wantErr: true,
			errMsg:  "partial/delta OTA detected",
		},
		{
			name: "RSR OTA rejected",
			info: &info.Info{
				Plists: &plist.Plists{
					OTAInfo: &plist.OTAInfo{
						MobileAssetProperties: types.Asset{
							SplatOnly: true,
						},
					},
				},
			},
			wantErr: true,
			errMsg:  "RSR",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateOTAScope(tt.info)
			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				if tt.errMsg != "" {
					if !strings.Contains(
						err.Error(), tt.errMsg,
					) {
						t.Errorf(
							"error %q should contain %q",
							err.Error(), tt.errMsg,
						)
					}
				}
			} else {
				if err != nil {
					t.Fatalf("unexpected error: %v", err)
				}
			}
		})
	}
}

func TestConfigureOTAContextFallbackFolderIncludesProduct(t *testing.T) {
	tmpDir := t.TempDir()
	ctx := &Context{}
	inf := &info.Info{
		Plists: &plist.Plists{
			AssetDataInfo: &plist.AssetDataInfo{
				ProductVersion: "26.5",
				Build:          "23F5043k",
				ProductType:    "iPhone18,1",
			},
		},
	}

	configureOTAContext(ctx, inf, tmpDir)

	if ctx.InputMode != inputModeOTA {
		t.Fatalf("InputMode = %v, want %v", ctx.InputMode, inputModeOTA)
	}
	if ctx.Version != "26.5" {
		t.Fatalf("Version = %q, want %q", ctx.Version, "26.5")
	}
	if ctx.Build != "23F5043k" {
		t.Fatalf("Build = %q, want %q", ctx.Build, "23F5043k")
	}

	wantFolder := filepath.Join(tmpDir, "23F5043k__iPhone18,1")
	if ctx.Folder != wantFolder {
		t.Fatalf("Folder = %q, want %q", ctx.Folder, wantFolder)
	}
}

func TestIsMacOSOTAFallsBackToProductSystemName(t *testing.T) {
	tests := []struct {
		name string
		info *info.Info
		want bool
	}{
		{
			name: "macOS OTA without BuildManifest",
			info: &info.Info{
				Plists: &plist.Plists{
					OTAInfo: &plist.OTAInfo{
						MobileAssetProperties: types.Asset{
							ProductSystemName: "macOS",
						},
					},
				},
			},
			want: true,
		},
		{
			name: "non-macOS OTA without BuildManifest",
			info: &info.Info{
				Plists: &plist.Plists{
					OTAInfo: &plist.OTAInfo{
						MobileAssetProperties: types.Asset{
							ProductSystemName: "iOS",
						},
					},
				},
			},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := isMacOSOTA(tt.info); got != tt.want {
				t.Fatalf("isMacOSOTA() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestCollectFeatureFlagsFromMountsSkipsMissingMountDirs(t *testing.T) {
	tmpDir := t.TempDir()
	systemRoot := filepath.Join(tmpDir, "system")
	featureDir := filepath.Join(systemRoot, "System", "Library", "FeatureFlags", "Domain")
	if err := os.MkdirAll(featureDir, 0o755); err != nil {
		t.Fatalf("MkdirAll() error = %v", err)
	}

	featurePath := filepath.Join(featureDir, "SoftwareUpdate.plist")
	wantContent := "<plist>system</plist>"
	if err := os.WriteFile(featurePath, []byte(wantContent), 0o644); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}

	// Test the single-mount helper directly: a missing FeatureFlags dir
	// should be skipped without erroring, and a present one should be walked.
	out := make(map[string]string)
	if err := collectFeatureFlagsFromMount(filepath.Join(tmpDir, "app-missing"), "AppOS", out); err != nil {
		t.Fatalf("collectFeatureFlagsFromMount(missing) error = %v", err)
	}
	if len(out) != 0 {
		t.Fatalf("missing-mount walk populated %v, want empty", out)
	}

	if err := collectFeatureFlagsFromMount(systemRoot, "SystemOS", out); err != nil {
		t.Fatalf("collectFeatureFlagsFromMount(present) error = %v", err)
	}

	got, ok := out[filepath.Join("Domain", "SoftwareUpdate.plist")]
	if !ok {
		t.Fatalf("expected extracted FeatureFlags entry, got map=%v", out)
	}
	if got != wantContent {
		t.Fatalf("FeatureFlags content = %q, want %q", got, wantContent)
	}
}

func TestWalkFolderFilesConfinesSymlinkDirsToRoot(t *testing.T) {
	root := t.TempDir()
	outside := t.TempDir()
	writeDiffTestFile(t, filepath.Join(root, "plain.txt"), "plain")
	writeDiffTestFile(t, filepath.Join(root, "real", "inside.txt"), "inside")
	writeDiffTestFile(t, filepath.Join(outside, "outside.txt"), "outside")

	if err := os.Symlink(outside, filepath.Join(root, "outside-link")); err != nil {
		t.Fatalf("symlink outside dir: %v", err)
	}
	if err := os.Symlink("/real", filepath.Join(root, "abs-real-link")); err != nil {
		t.Fatalf("symlink absolute in-root dir: %v", err)
	}

	var got []string
	if err := walkFolderFiles(root, func(relPath, _ string) error {
		got = append(got, filepath.ToSlash(relPath))
		return nil
	}); err != nil {
		t.Fatalf("walkFolderFiles() error = %v", err)
	}
	slices.Sort(got)
	want := []string{"plain.txt", "real/inside.txt"}
	if !slices.Equal(got, want) {
		t.Fatalf("walkFolderFiles() = %v, want %v", got, want)
	}
}

func TestOTAFirmwareMemberKeySourceQualifiesDuplicateBasenames(t *testing.T) {
	restore := search.FirmwareMemberKey(
		"AssetData/Firmware/image4/exclavecore_bundle.t8150.RELEASE.restore.im4p",
		"exclave_sharedcache",
	)
	release := search.FirmwareMemberKey(
		"AssetData/Firmware/image4/exclavecore_bundle.t8150.RELEASE.im4p",
		"exclave_sharedcache",
	)
	if restore == release {
		t.Fatalf("duplicate firmware basename keys collapsed: %q", restore)
	}
	if !strings.Contains(restore, "RELEASE.restore.im4p/exclave_sharedcache") {
		t.Fatalf("restore key %q does not retain containing member path", restore)
	}
}

func TestDetectInputMode(t *testing.T) {
	tmpDir := t.TempDir()

	// Create two temp directories.
	dir1 := filepath.Join(tmpDir, "dir1")
	dir2 := filepath.Join(tmpDir, "dir2")
	os.MkdirAll(dir1, 0o755)
	os.MkdirAll(dir2, 0o755)

	// Create two temp files (non-directories).
	file1 := filepath.Join(tmpDir, "file1.ipsw")
	file2 := filepath.Join(tmpDir, "file2.ipsw")
	os.WriteFile(file1, []byte("PK\x03\x04fake"), 0o644)
	os.WriteFile(file2, []byte("PK\x03\x04fake"), 0o644)

	tests := []struct {
		name     string
		old, new string
		wantMode inputMode
		wantErr  bool
	}{
		{
			name:     "both directories",
			old:      dir1,
			new:      dir2,
			wantMode: inputModeDirectory,
		},
		{
			name:     "both files (IPSW default)",
			old:      file1,
			new:      file2,
			wantMode: inputModeIPSW,
		},
		{
			name:    "mixed dir and file",
			old:     dir1,
			new:     file1,
			wantErr: true,
		},
		{
			name:    "nonexistent old",
			old:     filepath.Join(tmpDir, "nope"),
			new:     file1,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mode, err := detectInputMode(tt.old, tt.new)
			if tt.wantErr {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if mode != tt.wantMode {
				t.Errorf(
					"got mode %d, want %d",
					mode, tt.wantMode,
				)
			}
		})
	}
}

func TestTryOpenOTARejectsIPSW(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a minimal zip file pretending to be an IPSW.
	ipswPath := filepath.Join(tmpDir, "test.ipsw")
	os.WriteFile(ipswPath, []byte("PK\x03\x04fake"), 0o644)

	conf := &Config{}
	o, inf, err := tryOpenOTA(ipswPath, conf)
	if err != nil {
		t.Fatalf("expected nil error for .ipsw, got: %v", err)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle for .ipsw file")
	}
}

func TestTryOpenOTARejectsDMG(t *testing.T) {
	tmpDir := t.TempDir()

	// Test .dmg extension.
	dmgPath := filepath.Join(tmpDir, "test.dmg")
	os.WriteFile(dmgPath, []byte("fake"), 0o644)

	conf := &Config{}
	o, inf, err := tryOpenOTA(dmgPath, conf)
	if err != nil {
		t.Fatalf("expected nil error for .dmg, got: %v", err)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle for .dmg file")
	}

	// Test numeric DMG payload name.
	payloadPath := filepath.Join(
		tmpDir, "090-43228-337.dmg.aea",
	)
	os.WriteFile(payloadPath, []byte("fake"), 0o644)

	o, inf, err = tryOpenOTA(payloadPath, conf)
	if err != nil {
		t.Fatalf(
			"expected nil error for DMG payload, got: %v", err,
		)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle for DMG payload name")
	}
}

func TestTryOpenOTANonexistentFile(t *testing.T) {
	conf := &Config{}
	o, inf, err := tryOpenOTA("/nonexistent/path.ota", conf)
	if err == nil {
		t.Fatal("expected error for nonexistent file")
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle for nonexistent file")
	}
}

func TestTryOpenOTAAEAMagicPropagatesError(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a file with valid AEA magic but truncated/invalid
	// content. ota.Open should fail to decrypt, and since the
	// file has AEA magic, tryOpenOTA must propagate the error
	// rather than silently downgrading to "not an OTA".
	aeaPath := filepath.Join(tmpDir, "test.ota")
	// AEA1 magic: 0x41 0x45 0x41 0x31 (big-endian)
	aeaData := []byte{0x41, 0x45, 0x41, 0x31}
	// Pad with garbage to avoid trivial short-read rejections.
	aeaData = append(aeaData, make([]byte, 256)...)
	os.WriteFile(aeaPath, aeaData, 0o644)

	conf := &Config{}
	o, inf, err := tryOpenOTA(aeaPath, conf)
	if err == nil {
		// If handle was returned, close it to avoid leak.
		if o != nil {
			o.Close()
		}
		t.Fatal(
			"expected error for AEA file with invalid " +
				"content, got nil — decryption failure " +
				"should propagate, not be swallowed",
		)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle on error")
	}
	// Verify error mentions the key flags.
	if !strings.Contains(err.Error(), "--key-db") &&
		!strings.Contains(err.Error(), "failed to open") {
		t.Errorf(
			"error should mention key flags or open failure, "+
				"got: %v", err,
		)
	}
}

func TestTryOpenOTAAAMagicPropagatesError(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a file with valid YAA1 magic (little-endian:
	// 0x59 0x41 0x41 0x31) but invalid content.
	aaPath := filepath.Join(tmpDir, "test.ota")
	// YAA1 = 0x31414159 little-endian = bytes Y A A 1
	aaData := []byte{0x59, 0x41, 0x41, 0x31}
	aaData = append(aaData, make([]byte, 256)...)
	os.WriteFile(aaPath, aaData, 0o644)

	conf := &Config{}
	o, inf, err := tryOpenOTA(aaPath, conf)
	if err == nil {
		if o != nil {
			o.Close()
		}
		t.Fatal(
			"expected error for AA file with invalid " +
				"content, got nil — parse failure should " +
				"propagate for AA-magic files",
		)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle on error")
	}
}

func TestTryOpenOTAZipNonOTASilentMiss(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a file with ZIP magic but obviously not a real
	// ZIP (let alone an OTA). For ZIP-magic files, tryOpenOTA
	// should downgrade to nil, nil, nil — not propagate.
	zipPath := filepath.Join(tmpDir, "test.zip")
	// ZIP magic: PK\x03\x04
	zipData := []byte{0x50, 0x4b, 0x03, 0x04}
	zipData = append(zipData, make([]byte, 256)...)
	os.WriteFile(zipPath, zipData, 0o644)

	conf := &Config{}
	o, inf, err := tryOpenOTA(zipPath, conf)
	if err != nil {
		t.Fatalf(
			"expected silent miss for non-OTA ZIP, got: %v",
			err,
		)
	}
	if o != nil || inf != nil {
		t.Fatal("expected nil handle for non-OTA ZIP")
	}
}

func TestLookupAEAKeyUsesArchiveStemFromDownloadedFilename(t *testing.T) {
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "keys.json")
	data := `[{"filename":"e42aba0a3cb6684dc58eeef17d40bcdf987637c551987afd433ae1b31b1665f3.aea","key":"db-key"}]`
	if err := os.WriteFile(dbPath, []byte(data), 0o644); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}

	otaPath := filepath.Join(tmpDir, "iPhone18,1_23F5054h_e42aba0a3cb6684dc58eeef17d40bcdf987637c551987afd433ae1b31b1665f3.aea")
	key, err := lookupAEAKey(otaPath, dbPath)
	if err != nil {
		t.Fatalf("lookupAEAKey() error = %v", err)
	}
	if key != "db-key" {
		t.Fatalf("lookupAEAKey() = %q, want %q", key, "db-key")
	}
}
