package web

import (
	"strings"
	"testing"

	"github.com/matryer/is"
)

func TestRenderMarkdownWithURLRewriting(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "README.md",
	}

	// Test relative image in root README
	md := []byte(`![image](image.png)`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(string(html) != "")
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/image.png?raw=1"))

	// Test relative link to markdown file
	md = []byte(`[docs](docs/README.md)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/README.md"))

	// Test relative link to non-markdown file
	md = []byte(`[download](file.tar.gz)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/file.tar.gz?raw=1"))

	// Test relative link to directory
	md = []byte(`[folder](docs)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/tree/abc123def456/docs"))

	// Test absolute https URL unchanged
	md = []byte(`![remote](https://example.com/image.png)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "https://example.com/image.png"))

	// Test anchor-only link unchanged
	md = []byte(`[section](#heading)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "#heading"))

	// Test absolute path unchanged (starts with /)
	md = []byte(`[root](/other-repo/file)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/other-repo/file"))
}

func TestRenderMarkdownNestedReadme(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "docs/README.md",
	}

	// Test relative image in nested README
	md := []byte(`![image](image.png)`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/image.png?raw=1"))

	// Test going up with ../
	md = []byte(`![up](../root.png)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/root.png?raw=1"))

	// Test deep nesting
	md = []byte(`[deep](subdir/file.md)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/subdir/file.md"))
}

func TestRenderMarkdownTraversalAttempts(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "README.md",
	}

	// Test ../ traversal outside repo - should be blocked (empty src/href)
	md := []byte(`![escape](../../etc/passwd)`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	htmlStr := string(html)
	// Should not contain the path in src attribute
	is.True(!elementAttrContains(htmlStr, "img", "src", "etc/passwd"))

	// Test absolute path traversal
	md = []byte(`![abs](/../../../etc/passwd)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	htmlStr = string(html)
	// Absolute paths starting with / are left unchanged by rewriter
	is.True(elementAttrContains(htmlStr, "img", "src", "/../../../etc/passwd"))

	// Test that fragments don't leak when path traversal is blocked
	md = []byte(`![escape](../../secret.png#anchor)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	htmlStr = string(html)
	// Should not contain the path or fragment in src attribute
	is.True(!elementAttrContains(htmlStr, "img", "src", "secret.png") && !elementAttrContains(htmlStr, "img", "src", "#anchor"))
}

func TestRenderMarkdownURLDetails(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "README.md",
	}

	// Test fragments preserved for links
	md := []byte(`[section](docs/guide.md#installation)`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/guide.md#installation"))

	// Test fragments preserved for images
	md = []byte(`![diagram](docs/arch.png#diagram)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/arch.png?raw=1#diagram"))

	// Test directories link to /tree without trailing slash
	md = []byte(`[docs](docs/)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/tree/abc123def456/docs"))
	is.True(!strings.Contains(string(html), "docs/\"")) // no trailing slash in href

	// Test non-md files get ?raw=1
	md = []byte(`[archive](file.zip)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/file.zip?raw=1"))

	md = []byte(`[pdf](docs/manual.pdf)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/docs/manual.pdf?raw=1"))

	// Test uppercase .MD/.MARKDOWN files are treated as non-markdown
	// (document current behavior - filepath.Ext is case-sensitive)
	md = []byte(`[upper](README.MD)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	// .MD is not .md, so treated as regular file requiring ?raw=1
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/README.MD?raw=1"))

	md = []byte(`[markdown](README.MARKDOWN)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	// .MARKDOWN is not .markdown, so treated as regular file requiring ?raw=1
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/README.MARKDOWN?raw=1"))
}

func TestRenderMarkdownSpecialCharacters(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "README.md",
	}

	// Test spaces in filename - bluemonday may strip invalid URLs
	md := []byte(`![spaces](my_image.png)`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	// Should contain the repo path
	is.True(strings.Contains(string(html), "/test-repo/blob/abc123def456/"))

	// Test unicode
	md = []byte(`![unicode](文件.png)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(string(html) != "")
}

func TestRenderMarkdownDangerousSchemes(t *testing.T) {
	is := is.New(t)

	ctx := &ReadmeContext{
		RepoName:   "test-repo",
		CommitHash: "abc123def456",
		ReadmePath: "README.md",
	}

	// Test javascript: scheme - should be stripped by sanitizer
	md := []byte(`[xss](javascript:alert('xss'))`)
	html, err := renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(!strings.Contains(string(html), "javascript:"))

	// Test data: scheme - should be stripped
	md = []byte(`![data](data:image/png;base64,abc123)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	is.True(!strings.Contains(string(html), "data:image"))

	// Test http: scheme - should be stripped (only https allowed)
	md = []byte(`[http](http://example.com)`)
	html, err = renderMarkdown(md, ctx)
	is.NoErr(err)
	// Sanitizer should strip non-https schemes
	is.True(!strings.Contains(string(html), "http://example.com") || !strings.Contains(string(html), "href="))
}

func TestRenderMarkdownWithoutContext(t *testing.T) {
	is := is.New(t)

	// Test that rendering without context works (no rewriting)
	md := []byte(`![image](image.png)`)
	html, err := renderMarkdown(md, nil)
	is.NoErr(err)
	is.True(string(html) != "")
	// Without context, relative URLs stay relative
	is.True(strings.Contains(string(html), "image.png"))
}
