package server

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"os"
	"regexp"
	"strings"
	"testing"

	"git.autistici.org/id/auth"
	"git.autistici.org/id/keystore"
)

type fakeAuthClient struct{}

func (c *fakeAuthClient) Authenticate(_ context.Context, req *auth.Request) (*auth.Response, error) {
	p := string(req.Password)
	info := &auth.UserInfo{Shard: "shard1"}
	switch {
	case req.Username == "testuser" && p == "password":
		return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil
	case req.Username == "test2fa" && p == "password" && req.OTP == "123456":
		return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil
	case req.Username == "test2fa" && p == "password":
		return &auth.Response{
			Status:     auth.StatusInsufficientCredentials,
			TFAMethods: []auth.TFAMethod{auth.TFAMethodOTP},
		}, nil
	}

	return &auth.Response{Status: auth.StatusError}, nil
}

func createTestHTTPServer(t testing.TB, config *Config) *httptest.Server {
	svc, err := NewLoginService(config)
	if err != nil {
		t.Fatal("NewLoginService():", err)
	}

	srv, err := New(svc, &fakeAuthClient{}, config)
	if err != nil {
		t.Fatal("New():", err)
	}

	return httptest.NewTLSServer(srv.Handler())
}

func startTestHTTPServer(t testing.TB) (string, *httptest.Server) {
	tmpdir, _ := ioutil.TempDir("", "")
	config := testConfig(t, tmpdir, "")
	return tmpdir, createTestHTTPServer(t, config)
}

func startTestHTTPServerWithKeyStore(t testing.TB) (string, *httptest.Server) {
	ks := createFakeKeyStore(t, "testuser", "password")

	tmpdir, _ := ioutil.TempDir("", "")
	config := testConfig(t, tmpdir, ks.URL)
	return tmpdir, createTestHTTPServer(t, config)
}

func newTestHTTPClient() *http.Client {
	jar, _ := cookiejar.New(nil)
	transport := NewLoggedTransport(&http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}, DefaultLogger{Dump: true})
	return &http.Client{
		Jar:       jar,
		Transport: transport,
		// This client will only follow redirects to localhost.
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) > 10 {
				return errors.New("too many redirects")
			}
			if !strings.HasPrefix(req.URL.Host, "127.0.0.1:") {
				return http.ErrUseLastResponse
			}
			return nil
		},
	}
}

func TestHTTP_ServeStaticAsset(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()
	resp, err := c.Get(httpSrv.URL + "/static/js/u2f.js")
	if err != nil {
		t.Fatal("http.Get():", err)
	}
	if resp.StatusCode != 200 {
		t.Fatalf("bad status: %s", resp.Status)
	}
}

func doGet(t testing.TB, srv *httptest.Server, c *http.Client, relativeURL string, checkResponse ...func(testing.TB, *http.Response)) {
	resp, err := c.Get(srv.URL + relativeURL)
	if err != nil {
		t.Fatalf("http.Get(%s): %v", relativeURL, err)
	}
	defer resp.Body.Close()
	for _, f := range checkResponse {
		f(t, resp)
	}
}

func doPostForm(t testing.TB, srv *httptest.Server, c *http.Client, relativeURL string, v url.Values, checkResponse ...func(testing.TB, *http.Response)) {
	resp, err := c.PostForm(srv.URL+relativeURL, v)
	if err != nil {
		t.Fatalf("http.Get(%s): %v", relativeURL, err)
	}
	defer resp.Body.Close()
	for _, f := range checkResponse {
		f(t, resp)
	}
}

func checkStatusOk(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 200 {
		t.Fatalf("expected status 200, got %s", resp.Status)
	}
}

func checkRedirectToTargetService(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 302 {
		t.Fatalf("expected status 302, got %s", resp.Status)
	}
	if !strings.HasPrefix(resp.Header.Get("Location"), "https://service.example.com/sso_login?") {
		t.Fatalf("redirect is not to target service: %v", resp.Header.Get("Location"))
	}
}

var usernameFieldRx = regexp.MustCompile(`<input[^>]*name="username"`)

func checkLoginPasswordPage(t testing.TB, resp *http.Response) {
	if resp.Request.URL.Path != "/login" {
		t.Errorf("request path is not /login (%s)", resp.Request.URL.String())
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("reading body: %v", err)
	}
	if !usernameFieldRx.Match(data) {
		t.Fatalf("not the password login page:\n%s", string(data))
	}
}

var otpFieldRx = regexp.MustCompile(`<input[^>]*name="otp"`)

func checkLoginOTPPage(t testing.TB, resp *http.Response) {
	if resp.Request.URL.Path != "/login/otp" {
		t.Errorf("request path is not /login (%s)", resp.Request.URL.String())
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("reading body: %v", err)
	}
	if !otpFieldRx.Match(data) {
		t.Fatalf("not the OTP login page:\n%s", string(data))
	}
}

func TestHTTP_Login(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We expect the
	// result to be a 302 redirect to the target service.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)
}

func TestHTTP_LoginOnSecondAttempt(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login with wrong credentials.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "badpassword")
	doPostForm(t, httpSrv, c, "/login", v, checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We expect the
	// result to be a 302 redirect to the target service.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)
}

func TestHTTP_LoginAndLogout(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We expect the
	// result to be a 302 redirect to the target service.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)

	// Make a logout request.
	doGet(t, httpSrv, c, "/logout", checkStatusOk)

	// This new authorization request should send us to the login page.
	v = make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)
}

func TestHTTP_LoginOTP(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We should see the OTP page.
	v = make(url.Values)
	v.Set("username", "test2fa")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkStatusOk, checkLoginOTPPage)

	// Submit the correct OTP token. We expect the result to be a
	// 302 redirect to the target service.
	v = make(url.Values)
	v.Set("otp", "123456")
	doPostForm(t, httpSrv, c, "/login/otp", v, checkRedirectToTargetService)
}

func createFakeKeyStore(t testing.TB, username, password string) *httptest.Server {
	h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		if req.URL.Path != "/api/open" {
			http.NotFound(w, req)
			return
		}
		var openReq keystore.OpenRequest
		if err := json.NewDecoder(req.Body).Decode(&openReq); err != nil {
			t.Errorf("bad JSON body: %v", err)
			return
		}
		if openReq.Username != username {
			t.Errorf("bad username in keystore Open request: expected %s, got %s", username, openReq.Username)
		}
		if openReq.Password != password {
			t.Errorf("bad password in keystore Open request: expected %s, got %s", password, openReq.Password)
		}
		w.Header().Set("Content-Type", "application/json")
		io.WriteString(w, "{}") // nolint
	})
	return httptest.NewServer(h)
}

func TestHTTP_LoginWithKeyStore(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServerWithKeyStore(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We expect the
	// result to be a 302 redirect to the target service.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)
}

func TestHTTP_CORS(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// To test a CORS preflight request we have to login first.
	// Simulate an authorization request from a service, expect to
	// see the login page.
	v := make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage)

	// Attempt to login by submitting the form. We expect the
	// result to be a 302 redirect to the target service.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "password")
	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)

	// Simulate a CORS preflight request.
	v = make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	req, err := http.NewRequest("OPTIONS", httpSrv.URL+"/?"+v.Encode(), nil)
	if err != nil {
		t.Fatalf("NewRequest(): %v", err)
	}
	req.Header.Set("Origin", "https://origin.example.com")
	req.Header.Set("Access-Control-Request-Method", "GET")
	resp, err := c.Do(req)
	if err != nil {
		t.Fatalf("http request error: %v", err)
	}
	defer resp.Body.Close()
	checkStatusOk(t, resp)
	if s := resp.Header.Get("Access-Control-Allow-Origin"); s != "https://origin.example.com" {
		t.Fatalf("Bad Access-Control-Allow-Origin returned to OPTIONS request: %s", s)
	}
}