// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package urllib // import "miniflux.app/v2/internal/urllib"

import (
	"net"
	"net/url"
	"testing"
)

func TestIsRelativePath(t *testing.T) {
	scenarios := map[string]bool{
		// Valid relative paths
		"path/to/file.ext":    true,
		"./path/to/file.ext":  true,
		"../path/to/file.ext": true,
		"file.ext":            true,
		"./file.ext":          true,
		"../file.ext":         true,
		"/absolute/path":      true,
		"path?query=value":    true,
		"path#fragment":       true,
		"path?query#fragment": true,

		// Not relative paths
		"https://example.org/file.ext": false,
		"http://example.org/file.ext":  false,
		"//example.org/file.ext":       false,
		"//example.org":                false,
		`/\example.org`:                false,
		`\example.org`:                 false,
		`path\to\file.ext`:             false,
		"ftp://example.org/file.ext":   false,
		"mailto:user@example.org":      false,
		"magnet:?xt=urn:btih:example":  false,
		"":                             false,
		"magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C": false,
	}

	for input, expected := range scenarios {
		actual := IsRelativePath(input)
		if actual != expected {
			t.Errorf(`Unexpected result for IsRelativePath, got %v instead of %v for %q`, actual, expected, input)
		}
	}
}

func TestIsAbsoluteURL(t *testing.T) {
	scenarios := map[string]bool{
		"https://example.org/file.pdf":                   true,
		"https://example.org/file.pdf?download=1#page=2": true,
		"mailto:user@example.org":                        false,
		"data:text/plain,hello":                          false,
		"magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7": false,
		"invalid url":                false,
		"/relative/path":             false,
		"//example.org/path":         false,
		" https://example.org/path":  false,
		"\thttps://example.org/path": false,
	}

	for input, expected := range scenarios {
		actual := IsAbsoluteURL(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %v instead of %v for %q`, actual, expected, input)
		}
	}
}

func TestIsValidProxyURL(t *testing.T) {
	scenarios := map[string]bool{
		"http://127.0.0.1:3128":      true,
		"http://[::1]:1055":          true,
		"https://proxy.example.org":  true,
		"socks5://127.0.0.1:1080":    true,
		"socks5h://127.0.0.1:1080":   true,
		"socks5://[::1]:1055":        true,
		"socks5h://[::1]:1055":       true,
		"socks5://[::1%25eno1]:1055": true,
		"ftp://host":                 false,
		"/relative/path":             false,
		"invalid url":                false,
		"http://[::1":                false,
		"http:///var/run/socket":     false,
		"sock:///socket.file":        false,
		"socks5://[::1%eno1]:1055":   false,
		"":                           false,
	}

	for input, expected := range scenarios {
		actual := IsValidProxyURL(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %v instead of %v for %q`, actual, expected, input)
		}
	}
}

func TestAbsoluteURL(t *testing.T) {
	type absoluteScenario struct {
		name          string
		base          string
		relative      string
		expected      string
		wantErr       bool
		runWithParsed bool
		useNilParsed  bool
	}

	scenarios := []absoluteScenario{
		{"absolute path", "https://example.org/folder/", "/path/file.ext", "https://example.org/path/file.ext", false, true, false},
		{"relative path", "https://example.org/folder/", "path/file.ext", "https://example.org/folder/path/file.ext", false, true, false},
		{"dot path root", "https://example.org/path", "./", "https://example.org/", false, true, false},
		{"dot path folder", "https://example.org/folder/", "./", "https://example.org/folder/", false, true, false},
		{"missing slash in base", "https://example.org/folder", "path/file.ext", "https://example.org/path/file.ext", false, true, false},
		{"already absolute", "https://example.org/folder/", "https://example.org/path/file.ext", "https://example.org/path/file.ext", false, true, false},
		{"protocol relative", "https://www.example.org/", "//static.example.org/path/file.ext", "https://static.example.org/path/file.ext", false, true, false},
		{"magnet keeps scheme", "https://www.example.org/", "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a", "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a", false, true, false},
		{"magnet with query", "https://www.example.org/", "magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7", "magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7", false, true, false},
		{"empty relative returns base", "https://example.org/folder/", "", "https://example.org/folder/", false, true, false},
		{"invalid base errors", "://bad", "path/file.ext", "", true, false, false},
		{"absolute ignores invalid base", "://bad", "https://example.org/path/file.ext", "https://example.org/path/file.ext", false, true, true},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			actual, err := ResolveToAbsoluteURL(scenario.base, scenario.relative)
			if scenario.wantErr {
				if err == nil {
					t.Fatalf("expected error for base %q relative %q", scenario.base, scenario.relative)
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error for base %q relative %q: %v", scenario.base, scenario.relative, err)
			}
			if actual != scenario.expected {
				t.Fatalf("unexpected result, got %q instead of %q for (%q, %q)", actual, scenario.expected, scenario.base, scenario.relative)
			}

			if scenario.runWithParsed {
				var parsedBase *url.URL
				if !scenario.useNilParsed && scenario.base != "" {
					var parseErr error
					parsedBase, parseErr = url.Parse(scenario.base)
					if parseErr != nil {
						t.Fatalf("unable to parse base %q: %v", scenario.base, parseErr)
					}
				}

				actualParsed, errParsed := ResolveToAbsoluteURLWithParsedBaseURL(parsedBase, scenario.relative)
				if errParsed != nil {
					t.Fatalf("unexpected error with parsed base for (%q, %q): %v", scenario.base, scenario.relative, errParsed)
				}
				if actualParsed != scenario.expected {
					t.Fatalf("unexpected parsed-base result, got %q instead of %q for (%q, %q)", actualParsed, scenario.expected, scenario.base, scenario.relative)
				}
			}
		})
	}
}

func TestRootURL(t *testing.T) {
	scenarios := map[string]string{
		"":                                  "",
		"https://example.org/path/file.ext": "https://example.org/",
		"https://example.org/path/file.ext?test=abc": "https://example.org/",
		"//static.example.org/path/file.ext":         "https://static.example.org/",
		"https://example|org/path/file.ext":          "https://example|org/path/file.ext",
		"/relative/path":                             "/relative/path",
		"http://example.org:8080/path":               "http://example.org:8080/",
	}

	for input, expected := range scenarios {
		actual := RootURL(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %q instead of %q`, actual, expected)
		}
	}
}

func TestIsHTTPS(t *testing.T) {
	scenarios := map[string]bool{
		"https://example.org/": true,
		"http://example.org/":  false,
		"https://example|org/": false,
	}

	for input, expected := range scenarios {
		actual := IsHTTPS(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %v instead of %v`, actual, expected)
		}
	}
}

func TestDomain(t *testing.T) {
	scenarios := map[string]string{
		"https://static.example.org/": "static.example.org",
		"https://example|org/":        "https://example|org/",
	}

	for input, expected := range scenarios {
		actual := Domain(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %q instead of %q`, actual, expected)
		}
	}
}

func TestDomainWithoutWWW(t *testing.T) {
	scenarios := map[string]string{
		"https://www.example.org/":     "example.org",
		"https://example.org/":         "example.org",
		"https://www.sub.example.org/": "sub.example.org",
		"https://example|org/":         "https://example|org/",
	}

	for input, expected := range scenarios {
		actual := DomainWithoutWWW(input)
		if actual != expected {
			t.Errorf(`Unexpected result, got %q instead of %q`, actual, expected)
		}
	}
}

func TestJoinBaseURLAndPath(t *testing.T) {
	type args struct {
		baseURL string
		path    string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		wantErr bool
	}{
		{"empty base url", args{"", "/api/bookmarks/"}, "", true},
		{"empty path", args{"https://example.com", ""}, "", true},
		{"invalid base url", args{"incorrect url", ""}, "", true},
		{"valid", args{"https://example.com", "/api/bookmarks/"}, "https://example.com/api/bookmarks/", false},
		{"valid", args{"https://example.com/subfolder", "/api/bookmarks/"}, "https://example.com/subfolder/api/bookmarks/", false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := JoinBaseURLAndPath(tt.args.baseURL, tt.args.path)
			if (err != nil) != tt.wantErr {
				t.Errorf("JoinBaseURLAndPath error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("JoinBaseURLAndPath = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestIsNonPublicIP(t *testing.T) {
	testCases := []struct {
		name     string
		ipString string
		want     bool
	}{
		{"nil", "", true},
		{"private IPv4", "192.168.1.10", true},
		{"shared address space IPv4", "100.64.0.1", true},
		{"loopback IPv4", "127.0.0.1", true},
		{"link-local IPv4", "169.254.42.1", true},
		{"multicast IPv4", "224.0.0.1", true},
		{"unspecified IPv6", "::", true},
		{"loopback IPv6", "::1", true},
		{"multicast IPv6", "ff02::1", true},
		{"public IPv4", "93.184.216.34", false},
		{"public IPv6", "2001:4860:4860::8888", false},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var ip net.IP
			if tc.ipString != "" {
				ip = net.ParseIP(tc.ipString)
				if ip == nil {
					t.Fatalf("unable to parse %q", tc.ipString)
				}
			}

			if got := IsNonPublicIP(ip); got != tc.want {
				t.Fatalf("unexpected result for %s: got %v want %v", tc.name, got, tc.want)
			}
		})
	}
}
