package diff

import (
	"encoding/json"
	"os"
	"strings"
	"testing"

	mcmd "github.com/blacktop/ipsw/internal/commands/macho"
)

func newHTMLTestDiff(title string) *Diff {
	return New(&Config{
		Title:   title,
		IpswOld: "old.ipsw",
		IpswNew: "new.ipsw",
	})
}

func mustRenderHTML(t *testing.T, d *Diff) string {
	t.Helper()

	rendered, err := d.renderHTML()
	if err != nil {
		t.Fatalf("renderHTML returned error: %v", err)
	}

	return rendered
}

func TestRenderHTMLUsesStaticShellAndStripsMarkdownTOC(t *testing.T) {
	rendered := mustRenderHTML(t, newHTMLTestDiff("Example Diff"))

	if !strings.Contains(rendered, `class="report-shell"`) {
		t.Fatalf("rendered HTML missing report shell")
	}
	if strings.Contains(rendered, `cdn.jsdelivr.net/npm/bulma`) {
		t.Fatalf("rendered HTML should not include Bulma CDN")
	}
	if !strings.Contains(rendered, `<aside class="report-sidebar panel">`) {
		t.Fatalf("rendered HTML missing contents sidebar")
	}
}

func TestRenderHTMLTitleNotCorrupted(t *testing.T) {
	d := newHTMLTestDiff("23D8133__iPhone17,1 .vs 23D771330a__iPhone17,1")
	d.Old.IPSWPath = "/path/to/23D8133__iPhone17,1"
	d.New.IPSWPath = "/path/to/23D771330a__iPhone17,1"
	rendered := mustRenderHTML(t, d)

	if strings.Contains(rendered, "<strong>") {
		t.Fatalf("title corrupted: __ interpreted as markdown bold")
	}
	if !strings.Contains(rendered, "23D8133__iPhone17,1") {
		t.Fatalf("title missing literal underscores")
	}
}

func TestRenderHTMLTOCAnchorsWork(t *testing.T) {
	rendered := mustRenderHTML(t, newHTMLTestDiff("Test Diff"))

	if !strings.Contains(rendered, `href="#inputs"`) {
		t.Fatalf("TOC missing #inputs link")
	}
	if !strings.Contains(rendered, `id="inputs"`) {
		t.Fatalf("content missing id=inputs anchor")
	}
	if !strings.Contains(rendered, `href="#kernel"`) {
		t.Fatalf("TOC missing #kernel link")
	}
	if !strings.Contains(rendered, `id="kernel"`) {
		t.Fatalf("content missing id=kernel anchor")
	}
}

func TestRenderHTMLDiffHighlighting(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Dylibs = &mcmd.MachoDiff{
		Updated: map[string]string{
			"/System/Library/Frameworks/WebCore.framework/WebCore": "```diff\n+added line\n-removed line\n~modified line\n unchanged line\n```",
		},
	}
	rendered := mustRenderHTML(t, d)

	if !strings.Contains(rendered, `class="diff-add"`) {
		t.Fatalf("missing diff-add highlighting")
	}
	if !strings.Contains(rendered, `class="diff-del"`) {
		t.Fatalf("missing diff-del highlighting")
	}
	if !strings.Contains(rendered, `class="diff-mod"`) {
		t.Fatalf("missing diff-mod highlighting")
	}
	if strings.Contains(rendered, "```diff") || strings.Contains(rendered, "\n```</code></pre>") {
		t.Fatalf("diff fence markers leaked into rendered HTML")
	}
}

func TestRenderHTMLTextFenceHighlighting(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Dylibs = &mcmd.MachoDiff{
		Updated: map[string]string{
			"/System/Library/Frameworks/WebCore.framework/WebCore": "```text\nsummary line\n- __TEXT.__const\n~modified line\n```",
		},
	}
	rendered := mustRenderHTML(t, d)

	if !strings.Contains(rendered, `class="diff-mod"`) {
		t.Fatalf("missing modified-line highlighting in text fence")
	}
	if strings.Contains(rendered, `class="diff-del"`) {
		t.Fatalf("text-fence list bullet was highlighted as a deletion")
	}
	if strings.Contains(rendered, "```text") || strings.Contains(rendered, "\n```</code></pre>") {
		t.Fatalf("text fence markers leaked into rendered HTML")
	}
	if !strings.Contains(rendered, "summary line") {
		t.Fatalf("text-fence content was not rendered")
	}
	if !strings.Contains(rendered, "- __TEXT.__const") {
		t.Fatalf("text-fence list bullet was not rendered")
	}
}

func TestRenderHTMLMarkdownSectionList(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Dylibs = &mcmd.MachoDiff{
		Updated: map[string]string{
			"/System/Library/Frameworks/WebCore.framework/WebCore": "### Sections with Same Size but Changed Content\n\n- `__TEXT.__const`\n\n```diff\n+new string\n-old string\n```\n",
		},
	}
	rendered := mustRenderHTML(t, d)

	if !strings.Contains(rendered, "<h3>Sections with Same Size but Changed Content</h3>") {
		t.Fatalf("section summary heading was not rendered as Markdown")
	}
	if !strings.Contains(rendered, "<li><code>__TEXT.__const</code></li>") {
		t.Fatalf("section summary item was not rendered as a Markdown list")
	}
	if strings.Contains(rendered, "```text") {
		t.Fatalf("section summary unexpectedly contains a text fence")
	}
	if !strings.Contains(rendered, `class="diff-add"`) || !strings.Contains(rendered, `class="diff-del"`) {
		t.Fatalf("mixed section summary did not preserve diff highlighting")
	}
}

func TestRenderHTMLDetailsRendered(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Dylibs = &mcmd.MachoDiff{
		Updated: map[string]string{
			"/System/Library/Frameworks/Test.framework/Test": "some diff content",
		},
	}
	rendered := mustRenderHTML(t, d)

	if !strings.Contains(rendered, "<details>") {
		t.Fatalf("missing details element")
	}
	if !strings.Contains(rendered, "View Updated") {
		t.Fatalf("missing details summary text")
	}
	if !strings.Contains(rendered, "some diff content") {
		t.Fatalf("details content not rendered")
	}
	if strings.Contains(rendered, "```diff") {
		t.Fatalf("raw markdown code fence in HTML output")
	}
}

func TestRenderHTMLIncludesIBootFilesAndFeatureFlags(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Old.Version = "18.3.1"
	d.Old.Build = "23D8133"
	d.New.Version = "18.3.2"
	d.New.Build = "23E123"
	d.IBoot = &IBootDiff{
		Versions: []string{"iBoot-1000.0", "iBoot-1001.0"},
		New: map[string][]string{
			"iBoot": {"new-symbol"},
		},
	}
	d.Files = &FileDiff{
		New: map[string][]string{
			"filesystem": {"System/Library/NewFile"},
		},
		Removed: map[string][]string{
			"IPSW": {"Firmware/old.im4p"},
		},
	}
	d.Features = map[string]*PlistDiff{
		"SystemOS": {
			New: map[string]string{
				"/System/Library/FeatureFlags/Test.plist": "<plist><dict><key>Enabled</key><true/></dict></plist>",
			},
			Removed: []string{"/System/Library/FeatureFlags/Old.plist"},
			Updated: map[string]string{
				"/System/Library/FeatureFlags/Changed.plist": "```diff\n- old\n+ new\n```",
			},
		},
	}
	d.Localizations = map[string]*PlistDiff{
		"SystemOS": {
			Updated: map[string]string{
				"FileSystem/System/Library/PrivateFrameworks/CommunicationsFilter.framework/CommunicationsFilter.loctable": "```diff\n+en.BLOCKLIST_LIMIT_ALERT_TITLE = \"Blocked Contacts Limit Reached\"\n```",
			},
		},
	}
	rendered := mustRenderHTML(t, d)

	for _, needle := range []string{
		`id="iboot"`,
		`id="files"`,
		`id="feature-flags"`,
		`id="localizations"`,
		`<h5>CommunicationsFilter</h5>`,
		`new-symbol`,
		`System/Library/NewFile`,
		`Changed.plist`,
		`BLOCKLIST_LIMIT_ALERT_TITLE`,
	} {
		if !strings.Contains(rendered, needle) {
			t.Fatalf("rendered HTML missing %q", needle)
		}
	}
}

func TestRenderHTMLIncludesSandboxProfiles(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Sandbox = "### Collection\n\n#### Changed (1)\n\n##### locationd\n\n```diff\n-(deny default)\n+(allow default)\n```\n"

	rendered := mustRenderHTML(t, d)

	for _, needle := range []string{
		`href="#sandbox-profiles"`,
		`id="sandbox-profiles"`,
		`locationd`,
		`class="diff-add"`,
		`class="diff-del"`,
	} {
		if !strings.Contains(rendered, needle) {
			t.Fatalf("rendered HTML missing %q", needle)
		}
	}
}

func TestStringIncludesSandboxProfiles(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Sandbox = "### Collection\n\n#### New (1)\n\n##### locationd\n\n```scheme\n(version 1)\n```\n"

	rendered := d.String()

	for _, needle := range []string{
		"## Sandbox Profiles",
		"### Collection",
		"##### locationd",
	} {
		if !strings.Contains(rendered, needle) {
			t.Fatalf("rendered Markdown missing %q", needle)
		}
	}
}

func TestJSONIncludesSandboxProfiles(t *testing.T) {
	d := newHTMLTestDiff("Test Diff")
	d.Sandbox = "### Collection\n\n#### Changed (1)\n"

	data, err := json.Marshal(d)
	if err != nil {
		t.Fatalf("Marshal returned error: %v", err)
	}
	if !strings.Contains(string(data), `"sandbox"`) {
		t.Fatalf("JSON missing sandbox field: %s", data)
	}
}

func TestRenderSandboxProfileDiffMarkdown(t *testing.T) {
	oldDocs := sandboxProfileDocuments{
		sandboxDiffSourceCollection: {
			"locationd": "(version 1)\n(deny default)\n",
			"removed":   "(version 1)\n",
		},
	}
	newDocs := sandboxProfileDocuments{
		sandboxDiffSourceCollection: {
			"locationd": "(version 1)\n(allow default)\n",
			"added":     "(version 1)\n(deny default)\n",
		},
	}

	rendered, err := renderSandboxProfileDiffMarkdown(oldDocs, newDocs)
	if err != nil {
		t.Fatalf("renderSandboxProfileDiffMarkdown returned error: %v", err)
	}

	for _, needle := range []string{
		"### Collection",
		"#### New (1)",
		"##### added",
		"#### Removed (1)",
		"##### removed",
		"#### Changed (1)",
		"##### locationd",
		"+(allow default)",
		"-(deny default)",
	} {
		if !strings.Contains(rendered, needle) {
			t.Fatalf("rendered sandbox diff missing %q:\n%s", needle, rendered)
		}
	}
}

func TestTitleToFilenameSanitizesDiffReportName(t *testing.T) {
	d := New(&Config{
		Title: "23D8133__iPhone17,1 .vs 23D771330a__iPhone17,1",
	})

	if got := d.TitleToFilename(); got != "23D8133__iPhone17,1_vs_23D771330a__iPhone17,1" {
		t.Fatalf("TitleToFilename() = %q, want %q", got, "23D8133__iPhone17,1_vs_23D771330a__iPhone17,1")
	}
}

func TestGeneratePreviewHTML(t *testing.T) {
	if os.Getenv("GENERATE_PREVIEW") == "" {
		t.Skip("set GENERATE_PREVIEW=1 to generate preview HTML")
	}

	d := newHTMLTestDiff("23D8133__iPhone17,1 .vs 23D771330a__iPhone17,1")
	d.Old.IPSWPath = "/path/to/23D8133__iPhone17,1"
	d.New.IPSWPath = "/path/to/23D771330a__iPhone17,1"
	d.Old.Version = "18.3.1"
	d.Old.Build = "23D8133"
	d.Old.Webkit = "623.2.7.10.4"
	d.New.Version = "18.3.1"
	d.New.Build = "23D771330a"
	d.New.Webkit = "623.2.7.110.1"

	d.Dylibs = &mcmd.MachoDiff{
		Updated: map[string]string{
			"/System/Library/PrivateFrameworks/ProductKit.framework/ProductKit": `-129.400.11.2.4
-  __TEXT.__text: 0x64154
+129.400.11.2.2
+  __TEXT.__text: 0x63f94
   __TEXT.__auth_stubs: 0x1e80
-  __TEXT.__const: 0x5fcc
+  __TEXT.__const: 0x5fac
-  UUID: 16F203DB-1540-37D3-A425-AD4DB581E414
+  UUID: FF372DCB-64D0-369B-9F1B-38D3E4CC5B21
   Functions: 2178

Functions:
~ sub_2657d4504 : 6744 -> 6520
~ sub_2657d5f5c -> sub_2657d5e7c : 204 -> 172
CStrings:
- "Mac17,6"
- "Mac17,7"
- "iPad16,10"
- "iPad16,11"`,
			"/System/Library/PrivateFrameworks/WebCore.framework/WebCore": `-623.2.7.10.4
-  __TEXT.__text: 0x325ebe4
+623.2.7.110.1
+  __TEXT.__text: 0x325ef34
   __TEXT.__auth_stubs: 0xd910

Symbols:
+ __ZNK3WTF10RefCountedIN7WebCore20SWServerRegistrationEE5derefEv
- __ZN7WebCore16SWServerJobQueue27cancelJobsFromServiceWorkerEN3WTF23ObjectIdentifierGenericINS_27ServiceWorkerIdentifierTypeE
- __ZNK3WTF27RefCountedAndCanMakeWeakPtrIN7WebCore20SWServerRegistrationEE5derefEv`,
		},
	}

	rendered := mustRenderHTML(t, d)
	outPath := "/tmp/ipsw-diff-preview.html"
	if err := os.WriteFile(outPath, []byte(rendered), 0644); err != nil {
		t.Fatalf("failed to write preview: %v", err)
	}
	t.Logf("Preview written to %s", outPath)
}
