package tun2

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/Xe/uuid"
)

// testing constants
const (
	user           = "shachi"
	token          = "orcaz r kewl"
	noPermToken    = "aw heck"
	otherUserToken = "even more heck"
	domain         = "cetacean.club"
)

func TestNewServerNullConfig(t *testing.T) {
	_, err := NewServer(nil)
	if err == nil {
		t.Fatalf("expected NewServer(nil) to fail, got non-failure")
	}
}

func TestGen502Page(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	req, err := http.NewRequest("GET", "http://cetacean.club", nil)
	if err != nil {
		t.Fatal(err)
	}

	substring := uuid.New()

	req = req.WithContext(ctx)
	req.Header.Add("X-Request-Id", substring)
	req.Host = "cetacean.club"

	resp := gen502Page(req)
	if resp == nil {
		t.Fatalf("expected response to be non-nil")
	}

	if resp.Body != nil {
		defer resp.Body.Close()
		data, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Fatal(err)
		}

		if !strings.Contains(string(data), substring) {
			fmt.Println(string(data))
			t.Fatalf("502 page did not contain needed substring %q", substring)
		}
	}
}

func TestBackendAuthV1(t *testing.T) {
	st := MockStorage()

	s, err := NewServer(&ServerConfig{
		Storage: st,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	st.AddRoute(domain, user)
	st.AddToken(token, user, []string{"connect"})
	st.AddToken(noPermToken, user, nil)
	st.AddToken(otherUserToken, "cadey", []string{"connect"})

	cases := []struct {
		name    string
		auth    Auth
		wantErr bool
	}{
		{
			name: "basic everything should work",
			auth: Auth{
				Token:  token,
				Domain: domain,
			},
			wantErr: false,
		},
		{
			name: "invalid domain",
			auth: Auth{
				Token:  token,
				Domain: "aw.heck",
			},
			wantErr: true,
		},
		{
			name: "invalid token",
			auth: Auth{
				Token:  "asdfwtweg",
				Domain: domain,
			},
			wantErr: true,
		},
		{
			name: "invalid token scopes",
			auth: Auth{
				Token:  noPermToken,
				Domain: domain,
			},
			wantErr: true,
		},
		{
			name: "user token doesn't match domain owner",
			auth: Auth{
				Token:  otherUserToken,
				Domain: domain,
			},
			wantErr: true,
		},
	}

	for _, cs := range cases {
		t.Run(cs.name, func(t *testing.T) {
			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			data, err := json.Marshal(cs.auth)
			if err != nil {
				t.Fatal(err)
			}

			_, _, err = s.backendAuthv1(ctx, bytes.NewBuffer(data))

			if cs.wantErr && err == nil {
				t.Fatalf("auth did not err as expected")
			}

			if !cs.wantErr && err != nil {
				t.Fatalf("unexpected auth err: %v", err)
			}
		})
	}
}

func TestBackendRouting(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	st := MockStorage()

	st.AddRoute(domain, user)
	st.AddToken(token, user, []string{"connect"})

	s, err := NewServer(&ServerConfig{
		Storage: st,
	})
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()

	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer l.Close()

	go s.Listen(l, false)

	cases := []struct {
		name           string
		wantStatusCode int
		handler        http.HandlerFunc
	}{
		{
			name:           "200 everything's okay",
			wantStatusCode: http.StatusOK,
			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				http.Error(w, "HTTP 200, everything is okay :)", http.StatusOK)
			}),
		},
		{
			name:           "500 internal error",
			wantStatusCode: http.StatusInternalServerError,
			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				http.Error(w, "HTTP 500, the world is on fire", http.StatusInternalServerError)
			}),
		},
	}

	for _, cs := range cases {
		t.Run(cs.name, func(t *testing.T) {
			ts := httptest.NewServer(cs.handler)
			defer ts.Close()

			cc := &ClientConfig{
				ConnType:   "tcp",
				ServerAddr: l.Addr().String(),
				Token:      token,
				BackendURL: ts.URL,
				Domain:     domain,

				forceTCPClear: true,
			}

			c, err := NewClient(cc)
			if err != nil {
				t.Fatal(err)
			}

			go c.Connect(ctx) // TODO: fix the client library so this ends up actually getting cleaned up

			time.Sleep(125 * time.Millisecond)

			req, err := http.NewRequest("GET", "http://cetacean.club/", nil)
			if err != nil {
				t.Fatal(err)
			}

			resp, err := s.RoundTrip(req)
			if err != nil {
				t.Fatalf("error in doing round trip: %v", err)
			}

			if cs.wantStatusCode != resp.StatusCode {
				resp.Write(os.Stdout)
				t.Fatalf("got status %d instead of %d", resp.StatusCode, cs.wantStatusCode)
			}
		})
	}
}

func setupTestServer() (*Server, *mockStorage, net.Listener, error) {
	st := MockStorage()

	st.AddRoute(domain, user)
	st.AddToken(token, user, []string{"connect"})

	s, err := NewServer(&ServerConfig{
		Storage: st,
	})
	if err != nil {
		return nil, nil, nil, err
	}
	defer s.Close()

	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return nil, nil, nil, err
	}

	go s.Listen(l, false)

	return s, st, l, nil
}

func BenchmarkHTTP200(b *testing.B) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s, _, l, err := setupTestServer()
	if err != nil {
		b.Fatal(err)
	}
	defer s.Close()
	defer l.Close()

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
	defer ts.Close()

	cc := &ClientConfig{
		ConnType:   "tcp",
		ServerAddr: l.Addr().String(),
		Token:      token,
		BackendURL: ts.URL,
		Domain:     domain,

		forceTCPClear: true,
	}

	c, err := NewClient(cc)
	if err != nil {
		b.Fatal(err)
	}

	go c.Connect(ctx) // TODO: fix the client library so this ends up actually getting cleaned up

	for {
		r := s.GetBackendsForDomain(domain)
		if len(r) == 0 {
			time.Sleep(125 * time.Millisecond)
			continue
		}

		break
	}

	req, err := http.NewRequest("GET", "http://cetacean.club/", nil)
	if err != nil {
		b.Fatal(err)
	}

	_, err = s.RoundTrip(req)
	if err != nil {
		b.Fatalf("got error on initial request exchange: %v", err)
	}

	for n := 0; n < b.N; n++ {
		resp, err := s.RoundTrip(req)
		if err != nil {
			b.Fatalf("got error on %d: %v", n, err)
		}

		if resp.StatusCode != http.StatusOK {
			b.Fail()
			b.Logf("got %d instead of 200", resp.StatusCode)
		}
	}
}
