package gemini_test

import (
	"bytes"
	"errors"
	"io"
	"testing"

	"tildegit.org/tjp/sliderule/internal/types"
	"tildegit.org/tjp/sliderule/gemini"
)

func TestBuildResponses(t *testing.T) {
	table := []struct {
		name     string
		response *types.Response
		status   types.Status
		meta     string
		body     string
	}{
		{
			name:     "input response",
			response: gemini.Input("prompt here"),
			status:   gemini.StatusInput,
			meta:     "prompt here",
		},
		{
			name:     "sensitive input response",
			response: gemini.SensitiveInput("password please"),
			status:   gemini.StatusSensitiveInput,
			meta:     "password please",
		},
		{
			name:     "success response",
			response: gemini.Success("text/gemini", bytes.NewBufferString("body text here")),
			status:   gemini.StatusSuccess,
			meta:     "text/gemini",
			body:     "body text here",
		},
		{
			name:     "temporary redirect",
			response: gemini.Redirect("/foo/bar"),
			status:   gemini.StatusTemporaryRedirect,
			meta:     "/foo/bar",
		},
		{
			name:     "permanent redirect",
			response: gemini.PermanentRedirect("/baz/qux"),
			status:   gemini.StatusPermanentRedirect,
			meta:     "/baz/qux",
		},
		{
			name:     "fail response",
			response: gemini.Failure(errors.New("a failure")),
			status:   gemini.StatusTemporaryFailure,
			meta:     "a failure",
		},
		{
			name:     "server unavailable",
			response: gemini.Unavailable("server unavailable"),
			status:   gemini.StatusServerUnavailable,
			meta:     "server unavailable",
		},
		{
			name:     "cgi error",
			response: gemini.CGIError("some cgi error msg"),
			status:   gemini.StatusCGIError,
			meta:     "some cgi error msg",
		},
		{
			name:     "proxy error",
			response: gemini.ProxyError("upstream's full"),
			status:   gemini.StatusProxyError,
			meta:     "upstream's full",
		},
		{
			name:     "rate limiting",
			response: gemini.SlowDown(15),
			status:   gemini.StatusSlowDown,
			meta:     "15",
		},
		{
			name:     "permanent failure",
			response: gemini.PermanentFailure(errors.New("wut r u doin")),
			status:   gemini.StatusPermanentFailure,
			meta:     "wut r u doin",
		},
		{
			name:     "not found",
			response: gemini.NotFound("nope"),
			status:   gemini.StatusNotFound,
			meta:     "nope",
		},
		{
			name:     "gone",
			response: gemini.Gone("all out of that"),
			status:   gemini.StatusGone,
			meta:     "all out of that",
		},
		{
			name:     "refuse proxy",
			response: gemini.RefuseProxy("no I don't think I will"),
			status:   gemini.StatusProxyRequestRefused,
			meta:     "no I don't think I will",
		},
		{
			name:     "bad request",
			response: gemini.BadRequest("that don't make no sense"),
			status:   gemini.StatusBadRequest,
			meta:     "that don't make no sense",
		},
		{
			name:     "require cert",
			response: gemini.RequireCert("cert required"),
			status:   gemini.StatusClientCertificateRequired,
			meta:     "cert required",
		},
		{
			name:     "cert auth failure",
			response: gemini.CertAuthFailure("you can't see that"),
			status:   gemini.StatusCertificateNotAuthorized,
			meta:     "you can't see that",
		},
		{
			name:     "invalid cert",
			response: gemini.CertInvalid("bad cert dude"),
			status:   gemini.StatusCertificateNotValid,
			meta:     "bad cert dude",
		},
	}

	for _, test := range table {
		t.Run(test.name, func(t *testing.T) {
			if test.response.Status != test.status {
				t.Errorf("expected status %d, got %d", test.status, test.response.Status)
			}
			if test.response.Meta != test.meta {
				t.Errorf("expected meta %q, got %q", test.meta, test.response.Meta)
			}

			responseBytes, err := io.ReadAll(gemini.NewResponseReader(test.response))
			if err != nil {
				t.Fatalf("error reading response body: %q", err.Error())
			}

			body := string(bytes.SplitN(responseBytes, []byte("\r\n"), 2)[1])
			if body != test.body {
				t.Errorf("expected body %q, got %q", test.body, body)
			}
		})
	}
}

func TestParseResponses(t *testing.T) {
	table := []struct {
		input  string
		status types.Status
		meta   string
		body   string
		err    error
	}{
		{
			input:  "20 text/gemini\r\n# you got me!\n",
			status: gemini.StatusSuccess,
			meta:   "text/gemini",
			body:   "# you got me!\n",
		},
		{
			input:  "30 gemini://some.where/else\r\n",
			status: gemini.StatusTemporaryRedirect,
			meta:   "gemini://some.where/else",
		},
		{
			input: "10 forgot the line ending",
			err:   gemini.InvalidResponseLineEnding,
		},
		{
			input: "10 wrong line ending\n",
			err:   gemini.InvalidResponseLineEnding,
		},
		{
			input: "10no space\r\n",
			err:   gemini.InvalidResponseHeaderLine,
		},
		{
			input: "no status code\r\n",
			err:   gemini.InvalidResponseHeaderLine,
		},
		{
			input:  "31 gemini://domain.com/my/new/home\r\n",
			status: gemini.StatusPermanentRedirect,
			meta:   "gemini://domain.com/my/new/home",
		},
	}

	for _, test := range table {
		t.Run(test.input, func(t *testing.T) {
			response, err := gemini.ParseResponse(bytes.NewBufferString(test.input))

			if !errors.Is(err, test.err) {
				t.Fatalf("expected error %s, got %s", test.err, err)
			}

			if err != nil {
				return
			}

			if response.Status != test.status {
				t.Errorf("expected status %d, got %d", test.status, response.Status)
			}

			if response.Meta != test.meta {
				t.Errorf("expected meta %q, got %q", test.meta, response.Meta)
			}

			if response.Body == nil {
				if test.body != "" {
					t.Errorf("expected body %q, got nil", test.body)
				}
			} else {
				body, err := io.ReadAll(response.Body)
				if err != nil {
					t.Fatalf("error reading response body: %s", err.Error())
				}

				if test.body != string(body) {
					t.Errorf("expected body %q, got %q", test.body, string(body))
				}
			}
		})
	}
}

func TestResponseClose(t *testing.T) {
	body := &rdCloser{Buffer: bytes.NewBufferString("the body here")}
	resp := &types.Response{
		Status: gemini.StatusSuccess,
		Meta:   "text/gemini",
		Body:   body,
	}

	if err := resp.Close(); err != nil {
		t.Fatalf("response close error: %s", err.Error())
	}

	if !body.closed {
		t.Error("response body was not closed by response.Close()")
	}

	resp = &types.Response{
		Status: gemini.StatusInput,
		Meta:   "give me more",
	}

	if err := resp.Close(); err != nil {
		t.Fatalf("response close error: %s", err.Error())
	}
}

type rdCloser struct {
	*bytes.Buffer
	closed bool
}

func (rc *rdCloser) Close() error {
	rc.closed = true
	return nil
}

func TestResponseWriteTo(t *testing.T) {
	// invariant under test: WriteTo() sends the same bytes as Read()

	clone := func(resp *types.Response) *types.Response {
		other := &types.Response{
			Status: resp.Status,
			Meta:   resp.Meta,
		}

		if resp.Body != nil {
			// the body could be one-time readable, so replace it with a buffer
			buf, err := io.ReadAll(resp.Body)
			if err != nil {
				t.Fatalf("error reading response body: %s", err.Error())
			}
			resp.Body = bytes.NewBuffer(buf)

			buf2 := make([]byte, len(buf))
			if copy(buf2, buf) != len(buf) {
				t.Fatalf("short copy on a []byte")
			}

			other.Body = bytes.NewBuffer(buf2)
		}

		return other
	}

	table := []struct {
		name     string
		response *types.Response
	}{
		{
			name: "simple success",
			response: gemini.Success(
				"text/gemini",
				bytes.NewBufferString("the body goes here"),
			),
		},
		{
			name:     "no body",
			response: gemini.Input("need more pls"),
		},
	}

	for _, test := range table {
		t.Run(test.name, func(t *testing.T) {
			r1 := test.response
			r2 := clone(test.response)

			rdbuf, err := io.ReadAll(gemini.NewResponseReader(r1))
			if err != nil {
				t.Fatalf("response.Read(): %s", err.Error())
			}

			wtbuf := &bytes.Buffer{}
			if _, err := gemini.NewResponseReader(r2).WriteTo(wtbuf); err != nil {
				t.Fatalf("response.WriteTo(): %s", err.Error())
			}

			if wtbuf.String() != string(rdbuf) {
				t.Fatalf("Read produced %q but WriteTo produced %q", string(rdbuf), wtbuf.String())
			}
		})
	}
}