// This tests the swift package internals
//
// # It does not require access to a swift server
//
// FIXME need to add more tests and to check URLs and parameters
package swift

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"
)

const (
	TEST_ADDRESS = "localhost:5324"
	AUTH_URL     = "http://" + TEST_ADDRESS + "/v1.0"
	PROXY_URL    = "http://" + TEST_ADDRESS + "/proxy"
	USERNAME     = "test"
	APIKEY       = "apikey"
	AUTH_TOKEN   = "token"
)

// Globals
var (
	server *SwiftServer
	c      *Connection
)

// SwiftServer implements a test swift server
type SwiftServer struct {
	t      *testing.T
	checks []*Check
}

// Used to check and reply to http transactions
type Check struct {
	in  Headers
	out Headers
	rx  *string
	tx  *string
	err *Error
	url *string
}

// Add a in check
func (check *Check) In(in Headers) *Check {
	check.in = in
	return check
}

// Add an out check
func (check *Check) Out(out Headers) *Check {
	check.out = out
	return check
}

// Add an Error check
func (check *Check) Error(StatusCode int, Text string) *Check {
	check.err = newError(StatusCode, Text)
	return check
}

// Add a rx check
func (check *Check) Rx(rx string) *Check {
	check.rx = &rx
	return check
}

// Add an tx check
func (check *Check) Tx(tx string) *Check {
	check.tx = &tx
	return check
}

// Add an URL check
func (check *Check) Url(url string) *Check {
	check.url = &url
	return check
}

// Add a check
func (s *SwiftServer) AddCheck(t *testing.T) *Check {
	server.t = t
	check := &Check{
		in:  Headers{},
		out: Headers{},
		err: nil,
	}
	s.checks = append(s.checks, check)
	return check
}

// Responds to a request
func (s *SwiftServer) Respond(w http.ResponseWriter, r *http.Request) {
	if len(s.checks) < 1 {
		s.t.Fatal("Unexpected http transaction")
	}
	check := s.checks[0]
	s.checks = s.checks[1:]

	// Check URL
	if check.url != nil && *check.url != r.URL.String() {
		s.t.Errorf("Expecting URL %q but got %q", *check.url, r.URL)
	}

	// Check headers
	for k, v := range check.in {
		actual := r.Header.Get(k)
		if actual != v {
			s.t.Errorf("Expecting header %q=%q but got %q", k, v, actual)
		}
	}
	// Write output headers
	h := w.Header()
	for k, v := range check.out {
		h.Set(k, v)
	}
	// Return an error if required
	if check.err != nil {
		http.Error(w, check.err.Text, check.err.StatusCode)
	} else {
		if check.tx != nil {
			_, err := w.Write([]byte(*check.tx))
			if err != nil {
				s.t.Error("Write failed", err)
			}
		}
	}
}

// Checks to see all responses are used up
func (s *SwiftServer) Finished() {
	if len(s.checks) > 0 {
		s.t.Error("Unused checks", s.checks)
	}
}

func handle(w http.ResponseWriter, r *http.Request) {
	// out, _ := httputil.DumpRequest(r, true)
	// os.Stdout.Write(out)
	server.Respond(w, r)
}

func NewSwiftServer() *SwiftServer {
	server := &SwiftServer{}
	http.HandleFunc("/", handle)
	go func() {
		_ = http.ListenAndServe(TEST_ADDRESS, nil)
	}()
	fmt.Print("Waiting for server to start ")
	for {
		fmt.Print(".")
		conn, err := net.Dial("tcp", TEST_ADDRESS)
		if err == nil {
			_ = conn.Close()
			fmt.Println(" Started")
			break
		}
	}
	return server
}

func init() {
	server = NewSwiftServer()
	c = &Connection{
		UserName: USERNAME,
		ApiKey:   APIKEY,
		AuthUrl:  AUTH_URL,
	}
}

// Check the error is a swift error
func checkError(t *testing.T, err error, StatusCode int, Text string) {
	if err == nil {
		t.Fatal("No error returned")
	}
	err2, ok := err.(*Error)
	if !ok {
		t.Fatal("Bad error type")
	}
	if err2.StatusCode != StatusCode {
		t.Fatalf("Bad status code, expecting %d got %d", StatusCode, err2.StatusCode)
	}
	if err2.Text != Text {
		t.Fatalf("Bad error string, expecting %q got %q", Text, err2.Text)
	}
}

// FIXME copied from swift_test.go
func compareMaps(t *testing.T, a, b map[string]string) {
	if len(a) != len(b) {
		t.Error("Maps different sizes", a, b)
	}
	for ka, va := range a {
		if vb, ok := b[ka]; !ok || va != vb {
			t.Error("Difference in key", ka, va, b[ka])
		}
	}
	for kb, vb := range b {
		if va, ok := a[kb]; !ok || vb != va {
			t.Error("Difference in key", kb, vb, a[kb])
		}
	}
}

func TestInternalError(t *testing.T) {
	e := newError(404, "Not Found!")
	if e.StatusCode != 404 || e.Text != "Not Found!" {
		t.Fatal("Bad error")
	}
	if e.Error() != "Not Found!" {
		t.Fatal("Bad error")
	}

}

func testCheckClose(rd io.ReadCloser, e error) (err error) {
	err = e
	defer checkClose(rd, &err)
	return
}

// Make a closer which returns the error of our choice
type myCloser struct {
	err error
}

func (c *myCloser) Read([]byte) (int, error) {
	return 0, io.EOF
}

func (c *myCloser) Close() error {
	return c.err
}

func TestInternalCheckClose(t *testing.T) {
	if testCheckClose(&myCloser{nil}, nil) != nil {
		t.Fatal("bad 1")
	}
	if testCheckClose(&myCloser{nil}, ObjectCorrupted) != ObjectCorrupted {
		t.Fatal("bad 2")
	}
	if testCheckClose(&myCloser{ObjectNotFound}, nil) != ObjectNotFound {
		t.Fatal("bad 3")
	}
	if testCheckClose(&myCloser{ObjectNotFound}, ObjectCorrupted) != ObjectCorrupted {
		t.Fatal("bad 4")
	}
}

func TestInternalParseHeaders(t *testing.T) {
	resp := &http.Response{StatusCode: 200}
	if c.parseHeaders(resp, nil) != nil {
		t.Error("Bad 1")
	}
	if c.parseHeaders(resp, authErrorMap) != nil {
		t.Error("Bad 1")
	}

	resp = &http.Response{StatusCode: 299}
	if c.parseHeaders(resp, nil) != nil {
		t.Error("Bad 1")
	}

	resp = &http.Response{StatusCode: 199, Status: "BOOM"}
	checkError(t, c.parseHeaders(resp, nil), 199, "HTTP Error: 199: BOOM")

	resp = &http.Response{StatusCode: 300, Status: "BOOM"}
	checkError(t, c.parseHeaders(resp, nil), 300, "HTTP Error: 300: BOOM")

	resp = &http.Response{StatusCode: 404, Status: "BOOM"}
	checkError(t, c.parseHeaders(resp, nil), 404, "HTTP Error: 404: BOOM")
	if c.parseHeaders(resp, ContainerErrorMap) != ContainerNotFound {
		t.Error("Bad 1")
	}
	if c.parseHeaders(resp, objectErrorMap) != ObjectNotFound {
		t.Error("Bad 1")
	}
}

func TestInternalParseHeadersWithErrorMessageInBody(t *testing.T) {
	testCases := []struct {
		name     string
		resp     *http.Response
		errMap   map[int]error
		expected string
	}{
		{
			name: "Container error with body",
			resp: &http.Response{
				StatusCode: 400,
				Header: http.Header{
					"Content-Type": []string{"text/plain"},
				},
				ContentLength: 15,
				Body:          io.NopCloser(strings.NewReader("Body message")),
				Status:        "Status message",
			},
			errMap:   ContainerErrorMap,
			expected: "Bad Request: Body message",
		},
		{
			name: "Error with body",
			resp: &http.Response{
				StatusCode: 400,
				Header: http.Header{
					"Content-Type": []string{"text/plain"},
				},
				ContentLength: 15,
				Body:          io.NopCloser(strings.NewReader("Body message")),
				Status:        "Status message",
			},
			errMap:   nil,
			expected: "HTTP Error: 400: Status message: Body message",
		},
		{
			name: "Object error with body",
			resp: &http.Response{
				StatusCode: 400,
				Header: http.Header{
					"Content-Type": []string{"text/plain"},
				},
				ContentLength: 15,
				Body:          io.NopCloser(strings.NewReader("Body message")),
				Status:        "Status message",
			},
			errMap:   objectErrorMap,
			expected: "Bad Request: Body message",
		},
		{
			name: "Object error with body over 1024 bytes",
			resp: &http.Response{
				StatusCode: 400,
				Header: http.Header{
					"Content-Type": []string{"text/plain"},
				},
				ContentLength: 1025,
				Body:          io.NopCloser(strings.NewReader(strings.Repeat("a", 1025))),
				Status:        "Status message",
			},
			errMap:   objectErrorMap,
			expected: fmt.Sprintf("%s: %s", "Bad Request", strings.Repeat("a", 1024)),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			err := c.parseHeaders(tc.resp, tc.errMap)
			if err == nil {
				t.Fatalf("Expected error, got nil")
			}
			if err.Error() != tc.expected {
				t.Errorf("Expected error %q, got %q", tc.expected, err.Error())
			}
		})
	}
}

func TestInternalReadHeaders(t *testing.T) {
	resp := &http.Response{Header: http.Header{}}
	compareMaps(t, readHeaders(resp), Headers{})

	resp = &http.Response{Header: http.Header{
		"one": []string{"1"},
		"two": []string{"2"},
	}}
	compareMaps(t, readHeaders(resp), Headers{"one": "1", "two": "2"})

	// FIXME this outputs a log which we should test and check
	resp = &http.Response{Header: http.Header{
		"one": []string{"1", "11", "111"},
		"two": []string{"2"},
	}}
	compareMaps(t, readHeaders(resp), Headers{"one": "1", "two": "2"})
}

func TestInternalStorage(t *testing.T) {
	// FIXME
}

// ------------------------------------------------------------

func TestInternalAuthenticate(t *testing.T) {
	server.AddCheck(t).In(Headers{
		"User-Agent":  DefaultUserAgent,
		"X-Auth-Key":  APIKEY,
		"X-Auth-User": USERNAME,
	}).Out(Headers{
		"X-Storage-Url": PROXY_URL,
		"X-Auth-Token":  AUTH_TOKEN,
	}).Url("/v1.0")
	defer server.Finished()

	ctx := context.Background()
	err := c.Authenticate(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if c.StorageUrl != PROXY_URL {
		t.Error("Bad storage url")
	}
	if c.AuthToken != AUTH_TOKEN {
		t.Error("Bad auth token")
	}
	if !c.Authenticated() {
		t.Error("Didn't authenticate")
	}
}

func TestInternalAuthenticateDenied(t *testing.T) {
	server.AddCheck(t).Error(400, "Bad request")
	server.AddCheck(t).Error(401, "DENIED")
	defer server.Finished()
	c.UnAuthenticate()
	err := c.Authenticate(context.Background())
	if !errors.Is(err, AuthorizationFailed) {
		t.Fatal("Expecting AuthorizationFailed", err)
	}
	// FIXME
	// if c.Authenticated() {
	// 	t.Fatal("Expecting not authenticated")
	// }
}

func TestInternalAuthenticateBad(t *testing.T) {
	server.AddCheck(t).Out(Headers{
		"X-Storage-Url": PROXY_URL,
	})
	defer server.Finished()
	ctx := context.Background()
	err := c.Authenticate(ctx)
	checkError(t, err, 0, "Response didn't have storage url and auth token")
	if c.Authenticated() {
		t.Fatal("Expecting not authenticated")
	}

	server.AddCheck(t).Out(Headers{
		"X-Auth-Token": AUTH_TOKEN,
	})
	err = c.Authenticate(ctx)
	checkError(t, err, 0, "Response didn't have storage url and auth token")
	if c.Authenticated() {
		t.Fatal("Expecting not authenticated")
	}

	server.AddCheck(t)
	err = c.Authenticate(ctx)
	checkError(t, err, 0, "Response didn't have storage url and auth token")
	if c.Authenticated() {
		t.Fatal("Expecting not authenticated")
	}

	server.AddCheck(t).Out(Headers{
		"X-Storage-Url": PROXY_URL,
		"X-Auth-Token":  AUTH_TOKEN,
	})
	err = c.Authenticate(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if !c.Authenticated() {
		t.Fatal("Expecting authenticated")
	}
}

func testContainerNames(t *testing.T, rx string, expected []string) {
	server.AddCheck(t).In(Headers{
		"User-Agent":   DefaultUserAgent,
		"X-Auth-Token": AUTH_TOKEN,
	}).Tx(rx).Url("/proxy")
	containers, err := c.ContainerNames(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(containers) != len(expected) {
		t.Fatal("Wrong number of containers", len(containers), rx, len(expected), expected)
	}
	for i := range containers {
		if containers[i] != expected[i] {
			t.Error("Bad container", containers[i], expected[i])
		}
	}
}
func TestInternalContainerNames(t *testing.T) {
	defer server.Finished()
	testContainerNames(t, "", []string{})
	testContainerNames(t, "one", []string{"one"})
	testContainerNames(t, "one\n", []string{"one"})
	testContainerNames(t, "one\ntwo\nthree\n", []string{"one", "two", "three"})
}

func TestInternalObjectPutBytes(t *testing.T) {
	server.AddCheck(t).In(Headers{
		"User-Agent":     DefaultUserAgent,
		"X-Auth-Token":   AUTH_TOKEN,
		"Content-Length": "5",
		"Content-Type":   "text/plain",
	}).Rx("12345")
	defer server.Finished()
	err := c.ObjectPutBytes(context.Background(), "container", "object", []byte{'1', '2', '3', '4', '5'}, "text/plain")
	if err != nil {
		t.Fatal("ObjectPutBytes", err)
	}
}

func TestInternalObjectPutString(t *testing.T) {
	server.AddCheck(t).In(Headers{
		"User-Agent":     DefaultUserAgent,
		"X-Auth-Token":   AUTH_TOKEN,
		"Content-Length": "5",
		"Content-Type":   "text/plain",
	}).Rx("12345")
	defer server.Finished()
	err := c.ObjectPutString(context.Background(), "container", "object", "12345", "text/plain")
	if err != nil {
		t.Fatal(err)
	}
}

func TestSetFromEnv(t *testing.T) {
	// String
	s := ""

	_ = os.Setenv("POTATO", "")
	err := setFromEnv(&s, "POTATO")
	if err != nil {
		t.Fatal(err)
	}

	_ = os.Setenv("POTATO", "this is a test")
	err = setFromEnv(&s, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if s != "this is a test" {
		t.Fatal("incorrect", s)
	}

	_ = os.Setenv("POTATO", "new")
	err = setFromEnv(&s, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if s != "this is a test" {
		t.Fatal("was reset when it shouldn't have been")
	}

	// Integer
	i := 0

	_ = os.Setenv("POTATO", "42")
	err = setFromEnv(&i, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if i != 42 {
		t.Fatal("incorrect", i)
	}

	_ = os.Setenv("POTATO", "43")
	err = setFromEnv(&i, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if i != 42 {
		t.Fatal("was reset when it shouldn't have been")
	}

	i = 0
	_ = os.Setenv("POTATO", "not a number")
	err = setFromEnv(&i, "POTATO")
	if err == nil {
		t.Fatal("expecting error but didn't get one")
	}

	// bool
	var b bool
	_ = os.Setenv("POTATO", "1")
	err = setFromEnv(&b, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if b != true {
		t.Fatal("incorrect", b)
	}

	// time.Duration
	var dt time.Duration
	_ = os.Setenv("POTATO", "5s")
	err = setFromEnv(&dt, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if dt != 5*time.Second {
		t.Fatal("incorrect", dt)
	}

	// EndpointType
	var e EndpointType
	_ = os.Setenv("POTATO", "internal")
	err = setFromEnv(&e, "POTATO")
	if err != nil {
		t.Fatal(err)
	}
	if e != EndpointType("internal") {
		t.Fatal("incorrect", e)
	}

	// Unknown
	var unknown struct{}
	err = setFromEnv(&unknown, "POTATO")
	if err == nil {
		t.Fatal("expecting error")
	}

	_ = os.Setenv("POTATO", "")
}

func TestApplyEnvironment(t *testing.T) {
	// We've tested all the setting logic above, so just do a quick test here
	c := new(Connection)
	_ = os.Setenv("GOSWIFT_CONNECT_TIMEOUT", "100s")
	err := c.ApplyEnvironment()
	if err != nil {
		t.Fatal(err)
	}
	if c.ConnectTimeout != 100*time.Second {
		t.Fatal("timeout incorrect", c.ConnectTimeout)
	}

	c.ConnectTimeout = 0
	_ = os.Setenv("GOSWIFT_CONNECT_TIMEOUT", "parse error")
	err = c.ApplyEnvironment()
	if err == nil {
		t.Fatal("expecting error")
	}
	if c.ConnectTimeout != 0 {
		t.Fatal("timeout incorrect", c.ConnectTimeout)
	}

	_ = os.Setenv("GOSWIFT_CONNECT_TIMEOUT", "")
}

func TestApplyEnvironmentAll(t *testing.T) {
	// we do this in two phases because some of the variable set the same thing
	for phase := 1; phase <= 2; phase++ {
		c := new(Connection)

		items := []struct {
			phase    int
			result   interface{}
			name     string
			value    string
			want     interface{}
			oldValue string
		}{
			// Copied and amended from ApplyEnvironment
			// Environment variables - keep in same order as Connection
			{1, &c.Domain, "OS_USER_DOMAIN_NAME", "os_user_domain_name", "os_user_domain_name", ""},
			{1, &c.DomainId, "OS_USER_DOMAIN_ID", "os_user_domain_id", "os_user_domain_id", ""},
			{1, &c.UserName, "OS_USERNAME", "os_username", "os_username", ""},
			{1, &c.UserId, "OS_USER_ID", "os_user_id", "os_user_id", ""},
			{1, &c.ApiKey, "OS_PASSWORD", "os_password", "os_password", ""},
			{1, &c.AuthUrl, "OS_AUTH_URL", "os_auth_url", "os_auth_url", ""},
			{1, &c.Retries, "GOSWIFT_RETRIES", "4", 4, ""},
			{1, &c.UserAgent, "GOSWIFT_USER_AGENT", "goswift_user_agent", "goswift_user_agent", ""},
			{1, &c.ConnectTimeout, "GOSWIFT_CONNECT_TIMEOUT", "98s", 98 * time.Second, ""},
			{1, &c.Timeout, "GOSWIFT_TIMEOUT", "99s", 99 * time.Second, ""},
			{1, &c.Region, "OS_REGION_NAME", "os_region_name", "os_region_name", ""},
			{1, &c.AuthVersion, "ST_AUTH_VERSION", "3", 3, ""},
			{1, &c.Internal, "GOSWIFT_INTERNAL", "true", true, ""},
			{1, &c.Tenant, "OS_TENANT_NAME", "os_tenant_name", "os_tenant_name", ""},
			{2, &c.Tenant, "OS_PROJECT_NAME", "os_project_name", "os_project_name", ""},
			{1, &c.TenantId, "OS_TENANT_ID", "os_tenant_id", "os_tenant_id", ""},
			{1, &c.EndpointType, "OS_ENDPOINT_TYPE", "internal", EndpointTypeInternal, ""},
			{1, &c.TenantDomain, "OS_PROJECT_DOMAIN_NAME", "os_project_domain_name", "os_project_domain_name", ""},
			{1, &c.TenantDomainId, "OS_PROJECT_DOMAIN_ID", "os_project_domain_id", "os_project_domain_id", ""},
			{1, &c.TrustId, "OS_TRUST_ID", "os_trust_id", "os_trust_id", ""},
			{1, &c.StorageUrl, "OS_STORAGE_URL", "os_storage_url", "os_storage_url", ""},
			{1, &c.AuthToken, "OS_AUTH_TOKEN", "os_auth_token", "os_auth_token", ""},
			// v1 auth alternatives
			{2, &c.ApiKey, "ST_KEY", "st_key", "st_key", ""},
			{2, &c.UserName, "ST_USER", "st_user", "st_user", ""},
			{2, &c.AuthUrl, "ST_AUTH", "st_auth", "st_auth", ""},
		}

		for i := range items {
			item := &items[i]
			if item.phase == phase {
				item.oldValue = os.Getenv(item.name) // save old value
				_ = os.Setenv(item.name, item.value) // set new value
			}
		}

		err := c.ApplyEnvironment()
		if err != nil {
			t.Fatalf("unexpected error %v", err)
		}

		for i := range items {
			item := &items[i]
			if item.phase == phase {
				got := reflect.Indirect(reflect.ValueOf(item.result)).Interface()
				if !reflect.DeepEqual(item.want, got) {
					t.Errorf("%s: %v != %v", item.name, item.want, got)
				}
				_ = os.Setenv(item.name, item.oldValue) // restore old value
			}
		}
	}

}

func TestIsLastPage(t *testing.T) {
	conn := &Connection{}
	testPaging(t, conn, []pagingTest{
		{
			length:   0,
			limit:    1000,
			expected: true,
		},
		{
			length:   850,
			limit:    1000,
			expected: true,
		},
		{
			length:   950,
			limit:    1000,
			expected: true,
		},
		{
			length:   1000,
			limit:    1000,
			expected: false,
		},
		// For completeness
		{
			length:   0,
			limit:    0,
			expected: false,
		},
		{
			length:   2000,
			limit:    1000,
			expected: false,
		},
	})

}

func TestFetchUntilEmptyPageWorkaround(t *testing.T) {
	conn := &Connection{
		FetchUntilEmptyPage: true,
	}
	testPaging(t, conn, []pagingTest{
		{
			length:   0,
			limit:    1000,
			expected: true,
		},
		{
			length:   850,
			limit:    1000,
			expected: false,
		},
		{
			length:   950,
			limit:    1000,
			expected: false,
		},
		{
			length:   1000,
			limit:    1000,
			expected: false,
		},
		// For completeness
		{
			length:   0,
			limit:    0,
			expected: false,
		},
		{
			length:   2000,
			limit:    1000,
			expected: false,
		},
	})
}

func TestPartialPageFetchThreshold(t *testing.T) {
	conn := &Connection{
		PartialPageFetchThreshold: 90,
	}
	testPaging(t, conn, []pagingTest{
		{
			length:   0,
			limit:    1000,
			expected: true,
		},
		{
			length:   850,
			limit:    1000,
			expected: true,
		},
		{
			length:   950,
			limit:    1000,
			expected: false,
		},
		{
			length:   1000,
			limit:    1000,
			expected: false,
		},
		// For completeness
		{
			length:   0,
			limit:    0,
			expected: false,
		},
		{
			length:   2000,
			limit:    1000,
			expected: false,
		},
	})

}

type pagingTest struct {
	length   int
	limit    int
	expected bool
}

func testPaging(t *testing.T, conn *Connection, testCases []pagingTest) {
	for _, tCase := range testCases {
		if actual := conn.isLastPage(tCase.length, tCase.limit); actual != tCase.expected {
			t.Fatalf("isLastPage(%d, %d) returned %t, expected %t", tCase.length, tCase.limit, actual, tCase.expected)
		}
	}
}
