package proxy

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/goproxyio/goproxy/pkg/modfetch"
	"github.com/goproxyio/goproxy/pkg/module"
	"github.com/goproxyio/goproxy/pkg/testenv"
)

var _handle http.Handler

func TestMain(m *testing.M) {
	tmpdir, err := ioutil.TempDir("", "goproxy-test-")
	if err != nil {
		log.Fatalf("init tmpdir failed: %s", err)
	}
	defer os.RemoveAll(tmpdir)
	_handle = NewProxy(tmpdir)
	os.Exit(m.Run())
}

var _modInfoTests = []struct {
	path     string
	version  string
	latest   bool
	time     time.Time
	gomod    string
	zip      []string
	versions []string
}{
	{
		path:    "gopkg.in/check.v1",
		version: "v0.0.0-20161208181325-20d25e280405",
		time:    time.Date(2016, 12, 8, 18, 13, 25, 0, time.UTC),
		gomod:   "module gopkg.in/check.v1\n",
		zip: []string{
			".gitignore",
			".travis.yml",
			"LICENSE",
			"README.md",
			"TODO",
			"benchmark.go",
			"benchmark_test.go",
			"bootstrap_test.go",
			"check.go",
			"check_test.go",
			"checkers.go",
			"checkers_test.go",
			"export_test.go",
			"fixture_test.go",
			"foundation_test.go",
			"helpers.go",
			"helpers_test.go",
			"printer.go",
			"printer_test.go",
			"reporter.go",
			"reporter_test.go",
			"run.go",
			"run_test.go",
		},
	},
	{
		path:    "github.com/PuerkitoBio/goquery",
		version: "v0.0.0-20181014175806-2af3d16e2bb8",
		time:    time.Date(2018, 10, 14, 17, 58, 6, 0, time.UTC),
		gomod:   "module github.com/PuerkitoBio/goquery\n",
		zip: []string{
			".gitattributes",
			".gitignore",
			".travis.yml",
			"LICENSE",
			"README.md",
			"array.go",
			"array_test.go",
			"bench/v0.1.0",
			"bench/v0.1.1",
			"bench/v0.1.1-v0.2.1-go1.1rc1.svg",
			"bench/v0.2.0",
			"bench/v0.2.0-v0.2.1-go1.1rc1.svg",
			"bench/v0.2.1-go1.1rc1",
			"bench/v0.3.0",
			"bench/v0.3.2-go1.2",
			"bench/v0.3.2-go1.2-take2",
			"bench/v0.3.2-go1.2rc1",
			"bench/v1.0.0-go1.7",
			"bench/v1.0.1a-go1.7",
			"bench/v1.0.1b-go1.7",
			"bench/v1.0.1c-go1.7",
			"bench_array_test.go",
			"bench_example_test.go",
			"bench_expand_test.go",
			"bench_filter_test.go",
			"bench_iteration_test.go",
			"bench_property_test.go",
			"bench_query_test.go",
			"bench_traversal_test.go",
			"doc.go",
			"doc/tips.md",
			"example_test.go",
			"expand.go",
			"expand_test.go",
			"filter.go",
			"filter_test.go",
			"iteration.go",
			"iteration_test.go",
			"manipulation.go",
			"manipulation_test.go",
			"misc/git/pre-commit",
			"property.go",
			"property_test.go",
			"query.go",
			"query_test.go",
			"testdata/gotesting.html",
			"testdata/gowiki.html",
			"testdata/metalreview.html",
			"testdata/page.html",
			"testdata/page2.html",
			"testdata/page3.html",
			"traversal.go",
			"traversal_test.go",
			"type.go",
			"type_test.go",
			"utilities.go",
			"utilities_test.go",
		},
	},
	{
		path:    "github.com/rsc/vgotest1",
		version: "v0.0.0-20180219223237-a08abb797a67",
		latest:  true,
		time:    time.Date(2018, 02, 19, 22, 32, 37, 0, time.UTC),
	},
}

var _modListTests = []struct {
	path     string
	versions []string
}{
	{
		path:     "github.com/rsc/vgotest1",
		versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0", "v2.0.0+incompatible"},
	},
}

func TestFetchInfo(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	for _, mod := range _modInfoTests {
		req := buildRequest(mod.path, mod.version, ".info")

		rr, err := basicCheck(req)
		if err != nil {
			t.Error(err)
			continue
		}

		// check return data
		info := new(modfetch.RevInfo)
		if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
			t.Errorf("package info is not recognized")
			continue
		}
		if mod.version != info.Version {
			t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
		}
		if !mod.time.Equal(info.Time) {
			t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
		}
	}

}
func TestFetchModFile(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	for _, mod := range _modInfoTests {
		if len(mod.gomod) == 0 {
			continue
		}
		req := buildRequest(mod.path, mod.version, ".mod")

		rr, err := basicCheck(req)
		if err != nil {
			t.Error(err)
			continue
		}

		if data := rr.Body.String(); data != mod.gomod {
			t.Errorf("repo.GoMod(%q) = %q, want %q", mod.version, data, mod.gomod)
		}
	}
}
func TestFetchZip(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	for _, mod := range _modInfoTests {
		if len(mod.zip) == 0 {
			continue
		}
		req := buildRequest(mod.path, mod.version, ".zip")

		rr, err := basicCheck(req)
		if err != nil {
			t.Error(err)
			continue
		}

		prefix := mod.path + "@" + mod.version + "/"
		var names []string

		data := rr.Body.Bytes() // ??
		z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
		if err != nil {
			t.Errorf("open %s's zip failed: %v", mod.path, err)
			continue
		}

		for _, file := range z.File {
			if !strings.HasPrefix(file.Name, prefix) {
				t.Errorf("zip entry %v does not start with prefix %v", file.Name, prefix)
				continue
			}
			names = append(names, file.Name[len(prefix):])
		}
		if !reflect.DeepEqual(names, mod.zip) {
			t.Errorf("zip = %v\nwant %v\n", names, mod.zip)
		}
	}

}

func TestLatest(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	for _, mod := range _modInfoTests {
		if !mod.latest {
			continue
		}
		req := buildRequest(mod.path, "latest", "")

		rr, err := basicCheck(req)
		if err != nil {
			t.Error(err)
			continue
		}

		info := new(modfetch.RevInfo)
		if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
			t.Errorf("package info is not recognized")
			continue
		}
		if mod.version != info.Version {
			t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
		}
		if !mod.time.Equal(info.Time) {
			t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
		}
	}
}

func TestList(t *testing.T) {

	for _, mod := range _modListTests {
		req := buildRequest(mod.path, "", "")

		rr, err := basicCheck(req)
		if err != nil {
			t.Error(err)
			continue
		}

		modfetch.SortVersions(mod.versions)

		if data := rr.Body.String(); strings.Join(mod.versions, "\n") != data {
			t.Errorf("list not well,\n expected: %v\n, got: %v", mod.versions, strings.Split(data, "\n"))
		}
	}

}

func buildRequest(modPath, modVersion string, ext string) *http.Request {
	modPath, _ = module.EncodePath(modPath)
	modVersion, _ = module.EncodeVersion(modVersion)
	url := "/" + modPath
	switch modVersion {
	case "":
		url += "/@v/list"
	case "latest":
		url += "/@latest"
	default:
		url = url + "/@v/" + modVersion + ext
	}
	req, _ := http.NewRequest("GET", url, nil)
	return req
}

func basicCheck(req *http.Request) (*httptest.ResponseRecorder, error) {
	rr := httptest.NewRecorder()
	_handle.ServeHTTP(rr, req)

	// Check the status code is what we expect.
	if status := rr.Code; status != http.StatusOK {
		return nil, fmt.Errorf("handler returned wrong status code: got %v want %v",
			status, http.StatusOK)
	}
	return rr, nil
}