package read

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strings"
	"testing"
	"testing/iotest"

	"github.com/stretchr/testify/assert"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"

	"github.com/smallstep/linkedca"

	"github.com/smallstep/certificates/errs"
)

func TestJSON(t *testing.T) {
	type args struct {
		r io.Reader
		v interface{}
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{"ok", args{strings.NewReader(`{"foo":"bar"}`), make(map[string]interface{})}, false},
		{"fail", args{strings.NewReader(`{"foo"}`), make(map[string]interface{})}, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := JSON(tt.args.r, &tt.args.v)
			if (err != nil) != tt.wantErr {
				t.Errorf("JSON() error = %v, wantErr %v", err, tt.wantErr)
			}

			if tt.wantErr {
				var e *errs.Error
				if errors.As(err, &e) {
					if code := e.StatusCode(); code != 400 {
						t.Errorf("error.StatusCode() = %v, wants 400", code)
					}
				} else {
					t.Errorf("error type = %T, wants *Error", err)
				}
			} else if !reflect.DeepEqual(tt.args.v, map[string]interface{}{"foo": "bar"}) {
				t.Errorf("JSON value = %v, wants %v", tt.args.v, map[string]interface{}{"foo": "bar"})
			}
		})
	}
}

func TestProtoJSON(t *testing.T) {

	p := new(linkedca.Policy) // TODO(hs): can we use something different, so we don't need the import?

	type args struct {
		r io.Reader
		m proto.Message
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "fail/io.ReadAll",
			args: args{
				r: iotest.ErrReader(errors.New("read error")),
				m: p,
			},
			wantErr: true,
		},
		{
			name: "fail/proto",
			args: args{
				r: strings.NewReader(`{?}`),
				m: p,
			},
			wantErr: true,
		},
		{
			name: "ok",
			args: args{
				r: strings.NewReader(`{"x509":{}}`),
				m: p,
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ProtoJSON(tt.args.r, tt.args.m)
			if (err != nil) != tt.wantErr {
				t.Errorf("ProtoJSON() error = %v, wantErr %v", err, tt.wantErr)
			}

			if tt.wantErr {
				var (
					ee  *errs.Error
					bpe badProtoJSONError
				)
				switch {
				case errors.As(err, &bpe):
					assert.Contains(t, err.Error(), "syntax error")
				case errors.As(err, &ee):
					assert.Equal(t, http.StatusBadRequest, ee.Status)
				}
				return
			}

			assert.Equal(t, protoreflect.FullName("linkedca.Policy"), proto.MessageName(tt.args.m))
			assert.True(t, proto.Equal(&linkedca.Policy{X509: &linkedca.X509Policy{}}, tt.args.m))
		})
	}
}

func Test_badProtoJSONError_Render(t *testing.T) {
	tests := []struct {
		name     string
		e        badProtoJSONError
		expected string
	}{
		{
			name:     "bad proto normal space",
			e:        badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
			expected: "syntax error (line 1:2): invalid value ?",
		},
		{
			name:     "bad proto non breaking space",
			e:        badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
			expected: "syntax error (line 1:2): invalid value ?",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			w := httptest.NewRecorder()
			r := httptest.NewRequest("POST", "/test", http.NoBody)
			tt.e.Render(w, r)
			res := w.Result()
			defer res.Body.Close()

			data, err := io.ReadAll(res.Body)
			assert.NoError(t, err)

			v := struct {
				Type    string `json:"type"`
				Detail  string `json:"detail"`
				Message string `json:"message"`
			}{}

			assert.NoError(t, json.Unmarshal(data, &v))
			assert.Equal(t, "badRequest", v.Type)
			assert.Equal(t, "bad request", v.Detail)
			assert.Equal(t, "syntax error (line 1:2): invalid value ?", v.Message)

		})
	}
}
