package stfe

import (
	"bytes"
	"fmt"
	"strconv"
	"testing"

	"crypto/x509"
	"net/http"

	"github.com/google/trillian"
	"github.com/system-transparency/stfe/testdata"
)

// TODO: TestNewAddEntryRequest
func TestNewAddEntryRequest(t *testing.T) {
}

func TestNewGetEntriesRequest(t *testing.T) {
	lp := makeTestLogParameters(t, nil)
	for _, table := range []struct {
		description string
		start       string
		end         string
		wantErr     bool
	}{
		{
			description: "bad request: start must be an integer",
			start:       "start",
			end:         "10",
			wantErr:     true,
		},
		{
			description: "bad request: end must be an integer",
			start:       "10",
			end:         "end",
			wantErr:     true,
		},
		{
			description: "bad request: start must not be negative",
			start:       "-1",
			end:         "10",
			wantErr:     true,
		},
		{
			description: "bad request: start must be larger than end",
			start:       "1",
			end:         "0",
			wantErr:     true,
		},
		{
			description: "ok request but bad response: expected truncated",
			start:       "0",
			end:         fmt.Sprintf("%d", testMaxRange),
		},
		{
			description: "ok request and response",
			start:       "0",
			end:         "0",
		},
		{
			description: "ok request and response",
			start:       "0",
			end:         fmt.Sprintf("%d", testMaxRange-1),
		},
	} {
		r, err := http.NewRequest("GET", "http://example.com/"+lp.Prefix+"/get-entries", nil)
		if err != nil {
			t.Fatalf("must make http request in test %q: %v", table.description, err)
		}
		q := r.URL.Query()
		q.Add("start", table.start)
		q.Add("end", table.end)
		r.URL.RawQuery = q.Encode()

		req, err := lp.newGetEntriesRequest(r)
		if got, want := err != nil, table.wantErr; got != want {
			t.Errorf("got error is %v but wanted %v in test %q: %v", got, want, table.description, err)
		}
		if err != nil {
			continue
		}

		if got, want := req.Start, mustParseInt64(t, table.start); got != want {
			t.Errorf("got start %d but wanted %d in test %q", got, want, table.description)
		}
		if got, want := req.End, min(mustParseInt64(t, table.end), req.Start+testMaxRange-1); got != want {
			t.Errorf("got end %d but wanted %d in test %q", got, want, table.description)
		}
	}
}

func TestNewGetProofByHashRequest(t *testing.T) {
	lp := makeTestLogParameters(t, nil)
	for _, table := range []struct {
		description string
		treeSize    string
		hash        string
		wantErr     bool
	}{
		{
			description: "bad request: tree size must be an integer",
			treeSize:    "treeSize",
			hash:        b64(testNodeHash),
			wantErr:     true,
		},
		{
			description: "bad request: tree size must be larger than zero",
			treeSize:    "0",
			hash:        b64(testNodeHash),
			wantErr:     true,
		},
		{
			description: "bad request: hash is not base64",
			treeSize:    "1",
			hash:        "<(^_^)>",
			wantErr:     true,
		},
		{
			description: "bad request: invalid node hash (too small)",
			treeSize:    "1",
			hash:        b64(testNodeHash[1:]),
			wantErr:     true,
		},
		{
			description: "bad request: invalid node hash (too large)",
			treeSize:    "1",
			hash:        b64(append(testNodeHash, byte(0))),
			wantErr:     true,
		},
		{
			description: "ok request",
			treeSize:    "1",
			hash:        b64(testNodeHash),
		},
	} {
		r, err := http.NewRequest("GET", "http://example.com/"+lp.Prefix+"/get-proof-by-hash", nil)
		if err != nil {
			t.Fatalf("must make http request in test %q: %v", table.description, err)
		}
		q := r.URL.Query()
		q.Add("tree_size", table.treeSize)
		q.Add("hash", table.hash)
		r.URL.RawQuery = q.Encode()

		req, err := lp.newGetProofByHashRequest(r)
		if got, want := err != nil, table.wantErr; got != want {
			t.Errorf("got error is %v but wanted %v in test %q: %v", got, want, table.description, err)
		}
		if err != nil {
			continue
		}

		if got, want := req.TreeSize, mustParseInt64(t, table.treeSize); got != want {
			t.Errorf("got treeSize %d but wanted %d in test %q", got, want, table.description)
		}
		if got, want := req.Hash, mustDeb64(t, table.hash); !bytes.Equal(got, want) {
			t.Errorf("got hash %X but wanted %X in test %q", got, want, table.description)
		}
	}
}

func TestNewGetConsistencyProofRequest(t *testing.T) {
	lp := makeTestLogParameters(t, nil)
	for _, table := range []struct {
		description string
		first       string
		second      string
		wantErr     bool
	}{
		{
			description: "bad reuqest: first must be an integer",
			first:       "first",
			second:      "1",
			wantErr:     true,
		},
		{
			description: "bad request: second must be an integer",
			first:       "1",
			second:      "second",
			wantErr:     true,
		},
		{
			description: "bad request: first must be larger than zero",
			first:       "0",
			second:      "2",
			wantErr:     true,
		},
		{
			description: "bad request: second must be larger than firsst",
			first:       "2",
			second:      "1",
			wantErr:     true,
		},
		{
			description: "ok request",
			first:       "1",
			second:      "2",
		},
	} {
		r, err := http.NewRequest("GET", "http://example.com/"+lp.Prefix+"/get-consistency-proof", nil)
		if err != nil {
			t.Fatalf("must make http request in test %q: %v", table.description, err)
		}
		q := r.URL.Query()
		q.Add("first", table.first)
		q.Add("second", table.second)
		r.URL.RawQuery = q.Encode()

		req, err := lp.newGetConsistencyProofRequest(r)
		if got, want := err != nil, table.wantErr; got != want {
			t.Errorf("got error is %v but wanted %v in test %q: %v", got, want, table.description, err)
		}
		if err != nil {
			continue
		}

		if got, want := req.First, mustParseInt64(t, table.first); got != want {
			t.Errorf("got first %d but wanted %d in test %q", got, want, table.description)
		}
		if got, want := req.Second, mustParseInt64(t, table.second); got != want {
			t.Errorf("got second %d but wanted %d in test %q", got, want, table.description)
		}
	}
}

func TestNewGetEntryResponse(t *testing.T) {
	lp := makeTestLogParameters(t, nil)

	var appendix Appendix
	leaf, app := makeTestLeaf(t, testPackage, testdata.FirstPemChain, testdata.FirstPemChainKey)
	if err := appendix.Unmarshal(app); err != nil {
		t.Fatalf("must unmarshal appendix: %v", err)
	}
	if _, err := lp.newGetEntryResponse(leaf, app[1:]); err == nil {
		t.Errorf("got no error invalid appendix")
	}

	// Valid response
	rsp, err := lp.newGetEntryResponse(leaf, app)
	if err != nil {
		t.Errorf("got error %v but wanted none", err)
		return
	}
	if got, want := rsp.Item, leaf; !bytes.Equal(got, want) {
		t.Errorf("got leaf %X but wanted %X", got, want)
	}
	if got, want := rsp.Signature, appendix.Signature; !bytes.Equal(got, want) {
		t.Errorf("got signature %X but wanted %X", got, want)
	}
	if got, want := rsp.SignatureScheme, appendix.SignatureScheme; got != want {
		t.Errorf("got signature scheme %d but wanted %d", got, want)
	}
	if got, want := len(rsp.Chain), len(appendix.Chain); got != want {
		t.Errorf("got chain length %d but wanted %d", got, want)
	}
	for i, n := 0, len(rsp.Chain); i < n; i++ {
		if got, want := rsp.Chain[i], appendix.Chain[i].Data; !bytes.Equal(got, want) {
			t.Errorf("got chain[%d]=%X but wanted %X", i, got, want)
		}
	}
}

func TestNewGetEntriesResponse(t *testing.T) {
	lp := makeTestLogParameters(t, nil)

	// Invalid
	leaf := makeTrillianQueueLeafResponse(t, testPackage, testdata.FirstPemChain, testdata.FirstPemChainKey, false).QueuedLeaf.Leaf
	leaf.ExtraData = leaf.ExtraData[1:]
	if _, err := lp.newGetEntriesResponse([]*trillian.LogLeaf{leaf}); err == nil {
		t.Errorf("got no error for invalid appendix")
	}

	// Valid, including empty
	for n, numEntries := 0, 5; n < numEntries; n++ {
		leaves := make([]*trillian.LogLeaf, 0, n)
		for i := 0; i < n; i++ {
			leaves = append(leaves, makeTrillianQueueLeafResponse(t, []byte(fmt.Sprintf("%s-%d", testPackage, i)), testdata.FirstPemChain, testdata.FirstPemChainKey, false).QueuedLeaf.Leaf)
		}
		if rsp, err := lp.newGetEntriesResponse(leaves); err != nil {
			t.Errorf("got error for %d valid leaves: %v", n, err)
		} else if got, want := len(rsp), n; got != want {
			t.Errorf("got %d leaves but wanted %d", got, want)
		}
		// note that we tested actual leaf contents in TestNewGetEntryResponse
	}
}

func TestNewGetAnchorsResponse(t *testing.T) {
	rawAnchors := makeTestLogParameters(t, nil).newGetAnchorsResponse()
	if got, want := len(rawAnchors), testdata.NumPemAnchors; got != want {
		t.Errorf("got %d anchors but wanted %d", got, want)
	}
	for _, rawAnchor := range rawAnchors {
		if _, err := x509.ParseCertificate(rawAnchor); err != nil {
			t.Errorf("invalid trust anchor %X: %v", rawAnchor, err)
		}
	}
}

func mustParseInt64(t *testing.T, num string) int64 {
	n, err := strconv.ParseInt(num, 10, 64)
	if err != nil {
		t.Fatalf("must parse int: %v", err)
	}
	return n
}

func mustDeb64(t *testing.T, str string) []byte {
	b, err := deb64(str)
	if err != nil {
		t.Fatalf("must base64 decode: %v", err)
	}
	return b
}

func min(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}