package launchd

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/blacktop/go-plist"
)

func TestWalkVolumeLaunchdMachServices(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/System/Library/LaunchDaemons/com.apple.test.plist", map[string]any{
		"Label":              "com.apple.test",
		"CFBundleIdentifier": "com.apple.test.bundle",
		"ProgramArguments":   []string{"/usr/libexec/testd", "--flag"},
		"MachServices": map[string]any{
			"com.apple.test.b": true,
			"com.apple.test.a": map[string]any{"ResetAtClose": true},
		},
		"SandboxProfile": "testd",
		"POSIXSpawnType": "Adaptive",
		"RunAtLoad":      true,
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "SystemOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 1 {
		t.Fatalf("expected 1 record, got %d", len(records))
	}
	record := records[0]
	if record.SourceKind != SourceKindLaunchdDaemon {
		t.Fatalf("source kind = %q", record.SourceKind)
	}
	if record.Label != "com.apple.test" {
		t.Fatalf("label = %q", record.Label)
	}
	if record.BundleID != "com.apple.test.bundle" {
		t.Fatalf("bundle id = %q", record.BundleID)
	}
	if record.Program != "/usr/libexec/testd" {
		t.Fatalf("program = %q", record.Program)
	}
	if got, want := strings.Join(record.MachServices, ","), "com.apple.test.a,com.apple.test.b"; got != want {
		t.Fatalf("mach services = %q, want %q", got, want)
	}
	if record.SandboxProfile != "testd" {
		t.Fatalf("sandbox profile = %q", record.SandboxProfile)
	}
	if record.ServiceType != "Adaptive" {
		t.Fatalf("service type = %q", record.ServiceType)
	}
	if record.Extra["RunAtLoad"] != true {
		t.Fatalf("RunAtLoad missing from extra: %#v", record.Extra)
	}
	if _, ok := record.Extra["MachServices"]; ok {
		t.Fatalf("captured MachServices leaked into extra: %#v", record.Extra)
	}
	if _, ok := record.Extra["CFBundleIdentifier"]; ok {
		t.Fatalf("captured CFBundleIdentifier leaked into extra: %#v", record.Extra)
	}
}

func TestWalkVolumeXPCBundleLayouts(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/System/Library/PrivateFrameworks/Foo.framework/XPCServices/FooService.xpc/Info.plist", map[string]any{
		"CFBundleIdentifier": "com.apple.FooService",
		"CFBundleExecutable": "FooService",
		"XPCService": map[string]any{
			"ServiceType":         "Application",
			"SeatbeltProfiles":    []string{"foo-sandbox"},
			"JoinExistingSession": true,
		},
	}, plist.XMLFormat)
	writePlist(t, root, "/System/Library/PrivateFrameworks/Bar.framework/XPCServices/BarService.xpc/Contents/Info.plist", map[string]any{
		"CFBundleIdentifier": "com.apple.BarService",
		"CFBundleExecutable": "BarService",
		"XPCService": map[string]any{
			"ServiceType": "Application",
		},
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "SystemOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 2 {
		t.Fatalf("expected 2 records, got %d", len(records))
	}

	flat := findRecord(records, "/System/Library/PrivateFrameworks/Foo.framework/XPCServices/FooService.xpc/Info.plist")
	if flat == nil {
		t.Fatal("missing flat XPC record")
	}
	if flat.Program != "FooService" {
		t.Fatalf("flat program = %q", flat.Program)
	}
	if got := strings.Join(flat.MachServices, ","); got != "com.apple.FooService" {
		t.Fatalf("flat mach services = %q", got)
	}
	if flat.SandboxProfile != "foo-sandbox" {
		t.Fatalf("flat sandbox profile = %q", flat.SandboxProfile)
	}
	if flat.ServiceType != "Application" {
		t.Fatalf("flat service type = %q", flat.ServiceType)
	}
	xpcExtra, ok := flat.Extra["XPCService"].(map[string]any)
	if !ok || xpcExtra["JoinExistingSession"] != true {
		t.Fatalf("unexpected XPCService extra: %#v", flat.Extra["XPCService"])
	}
	if _, ok := xpcExtra["ServiceType"]; ok {
		t.Fatalf("captured ServiceType leaked into extra: %#v", xpcExtra)
	}

	contents := findRecord(records, "/System/Library/PrivateFrameworks/Bar.framework/XPCServices/BarService.xpc/Contents/Info.plist")
	if contents == nil {
		t.Fatal("missing Contents XPC record")
	}
	if contents.Program != "Contents/MacOS/BarService" {
		t.Fatalf("Contents program = %q", contents.Program)
	}
}

func TestWalkVolumeAPFSFuseRootPrefix(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/root/System/Library/LaunchDaemons/com.apple.rooted.plist", map[string]any{
		"Label":   "com.apple.rooted",
		"Program": "/usr/libexec/rooted",
	}, plist.XMLFormat)
	writePlist(t, root, "/root/Applications/Foo.app/XPCServices/FooService.xpc/Info.plist", map[string]any{
		"CFBundleIdentifier": "com.apple.FooService",
		"CFBundleExecutable": "FooService",
		"XPCService": map[string]any{
			"ServiceType": "Application",
		},
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "FileSystem")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 2 {
		t.Fatalf("expected launchd and XPC records under apfs-fuse root, got %d", len(records))
	}
	if findRecord(records, "/System/Library/LaunchDaemons/com.apple.rooted.plist") == nil {
		t.Fatalf("missing launchd daemon with normalized path: %#v", records)
	}
	if findRecord(records, "/Applications/Foo.app/XPCServices/FooService.xpc/Info.plist") == nil {
		t.Fatalf("missing XPC bundle with normalized path: %#v", records)
	}
	for _, record := range records {
		if strings.HasPrefix(record.PlistPath, "/root/") {
			t.Fatalf("plist_path kept apfs-fuse root prefix: %#v", record)
		}
	}
}

func TestWalkVolumeAppXPCAndPlainApp(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/Applications/HasService.app/Info.plist", map[string]any{
		"CFBundleIdentifier": "com.apple.HasService",
		"CFBundleExecutable": "HasService",
		"XPCService": map[string]any{
			"ServiceType": "Application",
		},
	}, plist.XMLFormat)
	writePlist(t, root, "/Applications/Plain.app/Info.plist", map[string]any{
		"CFBundleIdentifier": "com.apple.Plain",
		"CFBundleExecutable": "Plain",
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "AppOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 1 {
		t.Fatalf("expected only the app with XPCService, got %d records", len(records))
	}
	record := records[0]
	if record.SourceKind != SourceKindAppXPC {
		t.Fatalf("source kind = %q", record.SourceKind)
	}
	if record.Program != "HasService" {
		t.Fatalf("program = %q", record.Program)
	}
}

func TestWalkVolumeMissingProgram(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/System/Library/LaunchAgents/com.apple.noprogram.plist", map[string]any{
		"Label": "com.apple.noprogram",
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "SystemOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 1 {
		t.Fatalf("expected 1 record, got %d", len(records))
	}
	if records[0].Program != "" {
		t.Fatalf("program = %q", records[0].Program)
	}
}

func TestWalkVolumeProgramArgumentsKeepsEmptyFirstArgument(t *testing.T) {
	root := t.TempDir()
	writePlist(t, root, "/System/Library/LaunchDaemons/com.apple.emptyarg.plist", map[string]any{
		"Label":            "com.apple.emptyarg",
		"ProgramArguments": []any{"", "/usr/libexec/should-not-shift"},
	}, plist.XMLFormat)

	records, err := WalkVolume(root, "SystemOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 1 {
		t.Fatalf("expected 1 record, got %d", len(records))
	}
	if records[0].Program != "" {
		t.Fatalf("program = %q, want empty ProgramArguments[0]", records[0].Program)
	}
}

func TestWalkVolumeParseErrorPassthrough(t *testing.T) {
	root := t.TempDir()
	path := filepath.Join(root, "System/Library/LaunchAgents/bad.plist")
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte("not a plist"), 0644); err != nil {
		t.Fatal(err)
	}

	records, err := WalkVolume(root, "SystemOS")
	if err != nil {
		t.Fatal(err)
	}
	if len(records) != 1 {
		t.Fatalf("expected 1 record, got %d", len(records))
	}
	record := records[0]
	if record.SourceKind != SourceKindLaunchdAgent {
		t.Fatalf("source kind = %q", record.SourceKind)
	}
	if record.PlistDigest == "" {
		t.Fatal("parse error record has empty digest")
	}
	if record.Extra["parse_error"] == "" {
		t.Fatalf("parse_error missing from extra: %#v", record.Extra)
	}
}

func TestEncodeJSONLDeterministic(t *testing.T) {
	records := []Record{
		{
			SourceKind:     SourceKindXPCBundle,
			PlistPath:      "/b.plist",
			Volume:         "SystemOS",
			PlistDigest:    "2",
			MachServices:   []string{"z", "a"},
			SandboxProfile: "",
			Extra: map[string]any{
				"array":       []any{"b", "a"},
				"dict":        map[string]any{"z": []any{"2", "1"}},
				"dsl<filter>": "kept literal",
			},
		},
		{
			SourceKind:  SourceKindLaunchdDaemon,
			PlistPath:   "/a.plist",
			Volume:      "AppOS",
			PlistDigest: "1",
			Extra:       map[string]any{},
		},
	}

	first, err := EncodeJSONL(records)
	if err != nil {
		t.Fatal(err)
	}
	second, err := EncodeJSONL(records)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(first, second) {
		t.Fatalf("encoding changed across runs:\n%s\n%s", first, second)
	}
	lines := bytes.Split(bytes.TrimSuffix(first, []byte("\n")), []byte("\n"))
	if len(lines) != 2 {
		t.Fatalf("expected 2 lines, got %d", len(lines))
	}
	if !bytes.Contains(lines[0], []byte(`"volume":"AppOS"`)) {
		t.Fatalf("records not sorted by volume/path: %s", first)
	}
	if !bytes.Contains(lines[0], []byte(`"mach_services":[]`)) {
		t.Fatalf("empty mach services encoded as non-array: %s", lines[0])
	}
	if !bytes.Contains(lines[1], []byte(`"mach_services":["a","z"]`)) {
		t.Fatalf("mach services not sorted: %s", lines[1])
	}
	if !bytes.Contains(lines[1], []byte(`"array":["a","b"]`)) {
		t.Fatalf("extra array not sorted: %s", lines[1])
	}
	if bytes.Contains(lines[1], []byte(`\u003c`)) || bytes.Contains(lines[1], []byte(`\u003e`)) {
		t.Fatalf("HTML-sensitive characters were escaped: %s", lines[1])
	}

	var decoded map[string]any
	if err := json.Unmarshal(lines[1], &decoded); err != nil {
		t.Fatalf("invalid json: %v", err)
	}
}

func TestPlistDigestEquivalence(t *testing.T) {
	doc := map[string]any{
		"Label":     "com.apple.digest",
		"RunAtLoad": true,
		"MachServices": map[string]any{
			"com.apple.digest": true,
		},
	}
	xmlData, err := plist.Marshal(doc, plist.XMLFormat)
	if err != nil {
		t.Fatal(err)
	}
	binData, err := plist.Marshal(doc, plist.BinaryFormat)
	if err != nil {
		t.Fatal(err)
	}
	_, xmlDigest, err := parsePlist(xmlData)
	if err != nil {
		t.Fatal(err)
	}
	_, binDigest, err := parsePlist(binData)
	if err != nil {
		t.Fatal(err)
	}
	if xmlDigest != binDigest {
		t.Fatalf("digest mismatch: xml=%s bin=%s", xmlDigest, binDigest)
	}
}

func writePlist(t *testing.T, root, rel string, doc map[string]any, format int) {
	t.Helper()
	path := filepath.Join(root, filepath.FromSlash(strings.TrimPrefix(rel, "/")))
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		t.Fatal(err)
	}
	data, err := plist.Marshal(doc, format)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, data, 0644); err != nil {
		t.Fatal(err)
	}
}

func findRecord(records []Record, path string) *Record {
	for i := range records {
		if records[i].PlistPath == path {
			return &records[i]
		}
	}
	return nil
}
