package pkglint

import (
	"context"
	"errors"
	"gopkg.in/check.v1"
	"net"
	"net/http"
	"netbsd.org/pkglint/regex"
	"strconv"
	"syscall"
	"time"
)

func (s *Suite) Test_NewHomepageChecker(c *check.C) {
	t := s.Init(c)

	mklines := t.NewMkLines("filename.mk",
		"HOMEPAGE=\t# none")
	mkline := mklines.mklines[0]

	ck := NewHomepageChecker("value", "valueNoVar", mkline, mklines)

	t.CheckEquals(ck.Value, "value")
	t.CheckEquals(ck.ValueNoVar, "valueNoVar")
}

func (s *Suite) Test_HomepageChecker_Check(c *check.C) {
	t := s.Init(c)

	mklines := t.NewMkLines("filename.mk",
		"HOMEPAGE=\tftp://example.org/")
	mkline := mklines.mklines[0]
	value := mkline.Value()

	ck := NewHomepageChecker(value, value, mkline, mklines)

	ck.Check()

	t.CheckOutputLines(
		"WARN: filename.mk:1: An FTP URL is not a user-friendly homepage.")
}

func (s *Suite) Test_HomepageChecker_checkBasedOnMasterSites(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtHomepage)

	vt.Varname("HOMEPAGE")
	vt.Values(
		"${MASTER_SITES}")

	vt.Output(
		"WARN: filename.mk:1: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	pkg := NewPackage(t.File("category/package"))
	vt.Package(pkg)

	vt.Values(
		"${MASTER_SITES}")

	// When this assignment occurs while checking a package, but the package
	// doesn't define MASTER_SITES, that variable cannot be expanded, which means
	// the warning cannot suggest a replacement value.
	vt.Output(
		"WARN: filename.mk:11: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	pkg.vars.create("MASTER_SITES").firstDef = nil
	pkg.vars.create("MASTER_SITES").lastDef = nil
	pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
		"MASTER_SITES=\thttps://cdn.NetBSD.org/pub/pkgsrc/distfiles/"))

	vt.Values(
		"${MASTER_SITES}")

	vt.Output(
		"WARN: filename.mk:21: HOMEPAGE should not be defined in terms of MASTER_SITEs. " +
			"Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")

	pkg.vars.create("MASTER_SITES").firstDef = nil
	pkg.vars.create("MASTER_SITES").lastDef = nil
	pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
		"MASTER_SITES=\t${MASTER_SITE_GITHUB}"))

	vt.Values(
		"${MASTER_SITES}")

	// When MASTER_SITES itself makes use of another variable, pkglint doesn't
	// resolve that variable and just outputs the simple variant of this warning.
	vt.Output(
		"WARN: filename.mk:31: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	pkg.vars.create("MASTER_SITES").firstDef = nil
	pkg.vars.create("MASTER_SITES").lastDef = nil
	pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
		"MASTER_SITES=\t# none"))

	vt.Values(
		"${MASTER_SITES}")

	// When MASTER_SITES is empty, pkglint cannot extract the first of the URLs
	// for using it in the HOMEPAGE.
	vt.Output(
		"WARN: filename.mk:41: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
}

func (s *Suite) Test_HomepageChecker_checkFtp(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtHomepage)

	vt.Varname("HOMEPAGE")
	vt.Values(
		"ftp://example.org/",
		"ftp://example.org/ # no HTTP homepage available")

	vt.Output(
		"WARN: filename.mk:1: " +
			"An FTP URL is not a user-friendly homepage.")
}

func (s *Suite) Test_HomepageChecker_checkHttp(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtHomepage)

	vt.Varname("HOMEPAGE")
	vt.Values(
		"http://www.gnustep.org/",
		"http://www.pkgsrc.org/",
		"http://project.sourceforge.net/",
		"http://sf.net/p/project/",
		"http://sourceforge.net/p/project/",
		"http://example.org/ # doesn't support https",
		"http://example.org/ # only supports http",
		"http://asf.net/")

	vt.Output(
		"WARN: filename.mk:4: HOMEPAGE should migrate " +
			"from http://sf.net to https://sourceforge.net.")

	t.SetUpCommandLine("--autofix")
	vt.Values(
		"http://www.gnustep.org/",
		"http://www.pkgsrc.org/",
		"http://project.sourceforge.net/",
		"http://sf.net/p/project/",
		"http://sourceforge.net/p/project/",
		"http://example.org/ # doesn't support https",
		"http://example.org/ # only supports http",
		"http://kde.org/",
		"http://asf.net/")

	// www.gnustep.org does not support https at all.
	// www.pkgsrc.org is not in the (short) list of known https domains,
	// therefore pkglint does not dare to change it automatically.
	vt.Output(
		"AUTOFIX: filename.mk:14: Replacing \"http://sf.net\" "+
			"with \"https://sourceforge.net\".",
		"AUTOFIX: filename.mk:18: Replacing \"http\" with \"https\".")
}

func (s *Suite) Test_HomepageChecker_migrate(c *check.C) {
	t := s.Init(c)

	reachable := map[string]YesNoUnknown{}
	used := map[string]bool{}

	isReachable := func(url string) YesNoUnknown {
		if r, ok := reachable[url]; ok {
			used[url] = true
			return r
		}
		panic(url)
	}

	test := func(url string, migrate bool, from, to string) {
		ck := NewHomepageChecker(url, url, nil, nil)
		ck.isReachable = isReachable

		actualMigrate, actualFrom, actualTo := ck.migrate(url)

		t.CheckDeepEquals(
			[]interface{}{actualMigrate, actualFrom, actualTo},
			[]interface{}{migrate, from, to})

		for key, _ := range reachable {
			assertf(used[key], "Reachability of %q was not used.", key)
			delete(reachable, key)
		}
	}

	reachable["https://localhost/"] = unknown
	test(
		"http://localhost/",
		false,
		"",
		"")

	reachable["https://localhost/"] = yes
	test(
		"http://localhost/",
		true,
		"http",
		"https")

	reachable["https://project.sourceforge.io/"] = unknown
	test(
		"http://project.sourceforge.net/",
		false,
		"",
		"")

	reachable["https://project.sourceforge.io/"] = yes
	test(
		"http://project.sourceforge.net/",
		true,
		"http://project.sourceforge.net",
		"https://project.sourceforge.io")

	// To clean up the wrong autofix from 2020-01-18:
	// https://mail-index.netbsd.org/pkgsrc-changes/2020/01/18/msg205146.html
	reachable["https://project.sourceforge.io/"] = yes
	test(
		"https://project.sourceforge.net/",
		true,
		"sourceforge.net",
		"sourceforge.io")

	// To clean up the wrong autofix from 2020-01-18:
	// https://mail-index.netbsd.org/pkgsrc-changes/2020/01/18/msg205146.html
	//
	// If neither of the https URLs is reachable, the homepage
	// is rolled back to the http URL.
	reachable["https://project.sourceforge.io/"] = no
	reachable["http://project.sourceforge.net/"] = yes
	reachable["https://project.sourceforge.net/"] = no
	test(
		"https://project.sourceforge.net/",
		true,
		"https",
		"http")

	// To clean up the wrong autofix from 2020-01-18:
	// https://mail-index.netbsd.org/pkgsrc-changes/2020/01/18/msg205146.html
	//
	// If the https URL of sourceforge.net is reachable,
	// it is preferred over the http URL,
	// even though it is not mentioned in the SourceForge documentation.
	reachable["https://project.sourceforge.io/"] = no
	reachable["http://project.sourceforge.net/"] = yes
	reachable["https://project.sourceforge.net/"] = yes
	test(
		"https://project.sourceforge.net/",
		false,
		"",
		"")

	// To clean up the wrong autofix from 2020-01-18:
	// https://mail-index.netbsd.org/pkgsrc-changes/2020/01/18/msg205146.html
	reachable["https://project.sourceforge.io/"] = no
	reachable["http://project.sourceforge.net/"] = no
	test(
		"https://project.sourceforge.net/",
		false,
		"",
		"")

	// Since the URL contains a variable, it cannot be resolved.
	// Therefore it is skipped without sending any HTTP request.
	test(
		"http://godoc.org/${GO_SRCPATH}",
		false,
		"",
		"")
}

func (s *Suite) Test_HomepageChecker_checkBadUrls(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtHomepage)

	vt.Varname("HOMEPAGE")
	vt.Values(
		"http://garr.dl.sourceforge.net/project/name/dir/subdir/",
		"https://downloads.sourceforge.net/project/name/dir/subdir/",
		"https://cpan.metacpan.org/authors/id/I/IM/IMALPASS/")

	vt.Output(
		"WARN: filename.mk:1: A direct download URL is not a user-friendly homepage.",
		"WARN: filename.mk:2: A direct download URL is not a user-friendly homepage.",
		"WARN: filename.mk:3: A direct download URL is not a user-friendly homepage.")
}

func (s *Suite) Test_HomepageChecker_checkReachable(c *check.C) {
	t := s.Init(c)

	t.SetUpCommandLine("--network")

	mux := http.NewServeMux()
	mux.HandleFunc("/status/", func(writer http.ResponseWriter, request *http.Request) {
		location := request.URL.Query().Get("location")
		if location != "" {
			writer.Header().Set("Location", location)
		}

		status, err := strconv.Atoi(request.URL.Path[len("/status/"):])
		assertNil(err, "")
		writer.WriteHeader(status)
	})
	mux.HandleFunc("/timeout", func(http.ResponseWriter, *http.Request) {
		time.Sleep(2 * time.Second)
	})

	// 28780 = 256 * 'p' + 'l'
	srv := http.Server{Addr: "localhost:28780", Handler: mux}
	listener, err := net.Listen("tcp", srv.Addr)
	assertNil(err, "")
	shutdown := make(chan bool)

	go func() {
		err = srv.Serve(listener)
		assertf(err == http.ErrServerClosed, "%s", err)
		shutdown <- true
	}()

	defer func() {
		err := srv.Shutdown(context.Background())
		assertNil(err, "")
		<-shutdown
	}()

	test := func(url string, diagnostics ...string) {
		mklines := t.NewMkLines("filename.mk",
			"HOMEPAGE=\t"+url)
		mkline := mklines.mklines[0]
		ck := NewHomepageChecker(url, mkline.WithoutMakeVariables(url), mkline, mklines)
		ck.Timeout = 1 * time.Second
		ck.checkReachable()
		t.CheckOutput(diagnostics)
	}
	testMatches := func(url string, diagnostics ...regex.Pattern) {
		mklines := t.NewMkLines("filename.mk",
			"HOMEPAGE=\t"+url)
		ck := NewHomepageChecker(url, url, mklines.mklines[0], mklines)
		ck.Timeout = 1 * time.Second
		ck.checkReachable()
		t.CheckOutputMatches(diagnostics...)
	}

	test(
		"http://localhost:28780/status/200",
		nil...)

	test(
		"http://localhost:28780/status/301?location=/redirect301",
		"WARN: filename.mk:1: Homepage "+
			"\"http://localhost:28780/status/301?location=/redirect301\" "+
			"redirects to \"http://localhost:28780/redirect301\".")

	test(
		"http://localhost:28780/status/302?location=/redirect302",
		"WARN: filename.mk:1: Homepage "+
			"\"http://localhost:28780/status/302?location=/redirect302\" "+
			"redirects to \"http://localhost:28780/redirect302\".")

	test(
		"http://localhost:28780/status/307?location=/redirect307",
		"WARN: filename.mk:1: Homepage "+
			"\"http://localhost:28780/status/307?location=/redirect307\" "+
			"redirects to \"http://localhost:28780/redirect307\".")

	test(
		"http://localhost:28780/status/404",
		"WARN: filename.mk:1: Homepage \"http://localhost:28780/status/404\" "+
			"returns HTTP status \"404 Not Found\".")

	test(
		"http://localhost:28780/status/500",
		"WARN: filename.mk:1: Homepage \"http://localhost:28780/status/500\" "+
			"returns HTTP status \"500 Internal Server Error\".")

	test(
		"http://localhost:28780/timeout",
		"WARN: filename.mk:1: Homepage \"http://localhost:28780/timeout\" "+
			"cannot be checked: timeout")

	test(
		"http://localhost:28780/%invalid",
		"ERROR: filename.mk:1: Invalid URL \"http://localhost:28780/%invalid\".")

	testMatches(
		"http://localhost:28781/",
		// The "unknown network error" is for compatibility with Go < 1.13.
		`^WARN: filename\.mk:1: Homepage "http://localhost:28781/" `+
			`cannot be checked: (connection refused|timeout|unknown network error:.*)$`)

	testMatches(
		"https://no-such-name.example.org/",
		// The "unknown network error" is for compatibility with Go < 1.13.
		`^WARN: filename\.mk:1: Homepage "https://no-such-name.example.org/" `+
			`cannot be checked: `+
			`(name not found|timeout|connection refused|unknown network error:.*)$`)

	// Syntactically invalid URLs are silently skipped since VartypeCheck.URL
	// already warns about them.
	test(
		"https://!!!invalid/",
		nil...)

	// URLs with variables are skipped since they cannot be resolved in this form.
	test(
		"https://${SERVER}/",
		nil...)
}

func (s *Suite) Test_HomepageChecker_isReachableOnline(c *check.C) {
	t := s.Init(c)

	t.SetUpCommandLine("--network")

	mux := http.NewServeMux()
	mux.HandleFunc("/status/", func(writer http.ResponseWriter, request *http.Request) {
		location := request.URL.Query().Get("location")
		if location != "" {
			writer.Header().Set("Location", location)
		}

		status, err := strconv.Atoi(request.URL.Path[len("/status/"):])
		assertNil(err, "")
		writer.WriteHeader(status)
	})
	mux.HandleFunc("/timeout", func(http.ResponseWriter, *http.Request) {
		time.Sleep(1 * time.Second)
	})
	mux.HandleFunc("/ok/", func(http.ResponseWriter, *http.Request) {})

	// 28780 = 256 * 'p' + 'l'
	srv := http.Server{Addr: "localhost:28780", Handler: mux}
	listener, err := net.Listen("tcp", srv.Addr)
	assertNil(err, "")
	shutdown := make(chan bool)

	go func() {
		err := srv.Serve(listener)
		assertf(err == http.ErrServerClosed, "%s", err)
		shutdown <- true
	}()

	defer func() {
		err := srv.Shutdown(context.Background())
		assertNil(err, "")
		<-shutdown
	}()

	test := func(url string, reachable YesNoUnknown) {
		ck := NewHomepageChecker(url, url, nil, nil)
		ck.Timeout = 500 * time.Millisecond
		actual := ck.isReachableOnline(url)

		t.CheckEquals(actual, reachable)
	}

	test("http://localhost:28780/status/200", yes)
	test("http://localhost:28780/status/301?location=/", no)
	test("http://localhost:28780/status/404", no)
	test("http://localhost:28780/status/500", no)
	test("http://localhost:28780/timeout", no)
	test("http://localhost:28780/ok/${VAR}", unknown)
	test("http://localhost:28780/ invalid", unknown)
	test("http://localhost:28780/%invalid", no)
	test("http://localhost:28781/", no)
}

func (s *Suite) Test_HomepageChecker_hasAnySuffix(c *check.C) {
	t := s.Init(c)

	test := func(s string, hasAnySuffix bool, suffix string) {
		actual := (*HomepageChecker).hasAnySuffix(nil, s, suffix)

		t.CheckEquals(actual, hasAnySuffix)
	}

	test("example.org", true, "org")
	test("example.com", false, "org")
	test("example.org", true, "example.org")
	test("example.org", false, ".example.org")
	test("example.org", true, ".org")
	test("borg", false, "org")
}

func (s *Suite) Test_HomepageChecker_classifyNetworkError(c *check.C) {
	t := s.Init(c)

	test := func(err error, expectedClass string) {
		actual := (*HomepageChecker).classifyNetworkError(nil, err)

		t.CheckEquals(actual, expectedClass)
	}

	test(syscall.Errno(10061), "connection refused")
	test(syscall.ECONNREFUSED, "connection refused")
	test(syscall.ECONNRESET, "unknown network error: connection reset by peer")
	test(errors.New("unknown"), "unknown network error: unknown")
	test(&net.AddrError{"msg", "addr"}, "unknown network error: address addr: msg")
	test(&net.DNSError{Err: "msg", Name: "name"}, "unknown network error: lookup name: msg")
}
