package controllers

import (
	"context"
	"errors"
	"io"
	"io/ioutil"
	"net/http"
	"strings"
	"testing"

	"github.com/golang/protobuf/ptypes"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	spamcheck "gitlab.com/gitlab-org/spamcheck/api/v1"
	config "gitlab.com/gitlab-org/spamcheck/app/config"
)

var testGRPC = &config.GRPC{
	Port: "5050",
}

var testREST = &config.REST{
	ExternalPort : "5051",
}

var testInspector = &config.Inspector{
	InspectorURL: "fake.url",
}

var testLogger = &config.Logger{
	Level: "info",
	Output: "json",
	Format: "stderr",
}

var testMonitor = &config.Monitor{
	Address: "5053",
}

var testExtraAttributes = &config.ExtraAttributes{
	MonitorMode: "false",
}

var testFilter = &config.Filter{
	AllowList: map[string]string{"14": "spamtest/hello"},
	DenyList: map[string]string{},
}

var testConfig = &config.Config{
	*testGRPC,
	*testREST,
	*testLogger,
	*testMonitor,
	*testInspector,
	*testExtraAttributes,
	*testFilter,
}

type MockHTTPClient struct {
	mock.Mock
}

func (m *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
	args := m.Called(url, contentType, body)
	return args.Get(0).(*http.Response), args.Error(1)
}

func TestCheckForSpamIssue(t *testing.T) {

	gitlab_employee := spamcheck.User{
		Username: "Spaceman Spiff",
		Emails: []*spamcheck.User_Email{
			{
				Email:    "spaceman.spiff@gitlab.com",
				Verified: true,
			},
		},
		Org:       "gitlab",
		CreatedAt: ptypes.TimestampNow(),
	}

	user := spamcheck.User{
		Username: "Mr. Stupendous",
		Emails: []*spamcheck.User_Email{
			{
				Email:    "mr.stupendous@email.com",
				Verified: true,
			},
		},
		Org:       "gitlab",
		CreatedAt: ptypes.TimestampNow(),
	}

	project := spamcheck.Project{
		ProjectId:     14,
		ProjectPath:   "spamtest/hello",
	}

	project1 := spamcheck.Project{
		ProjectId:     21,
		ProjectPath:   "testy/test",
	}

	t.Run("testing with non-spam Issue", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		jsonResponse := `{
			"1": {
				"ham": 0.99,
				"spam": 0.01
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)
		Client = mockHTTPClient

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_ALLOW

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with spam Issue", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		jsonResponse := `{
			"1": {
				"ham": 0.01,
				"spam": 0.99
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)
		Client = mockHTTPClient

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_BLOCK

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with likely spam Issue", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient

		jsonResponse := `{
			"1": {
				"ham": 0.51,
				"spam": 0.49
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_CONDITIONAL_ALLOW

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with highly likely spam Issue", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient
		jsonResponse := `{
			"1": {
				"ham": 0.41,
				"spam": 0.59
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_DISALLOW

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with populated AL, empty DL and matching AL", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient

		jsonResponse := `{
			"1": {
				"ham": 0.01,
				"spam": 0.99
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_BLOCK

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with populated AL, empty DL and not matching AL", func(t *testing.T) {
		controller := CreateSpamCheckController(testConfig)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project1,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_NOOP

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with empty AL, populated DL and matching DL", func(t *testing.T) {
		var testFilter1 = &config.Filter{
			AllowList: map[string]string{},
        	DenyList: map[string]string{"21": "testy/test"},
		}

		var testConfig1 = &config.Config{
			*testGRPC,
			*testREST,
			*testLogger,
			*testMonitor,
			*testInspector,
			*testExtraAttributes,
			*testFilter1,
		}

		controller := CreateSpamCheckController(testConfig1)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project1,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_NOOP

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with empty AL, populated DL and not matching DL", func(t *testing.T) {
		var testFilter1 = &config.Filter{
			AllowList: map[string]string{},
        	DenyList: map[string]string{"21": "testy/test"},
		}

		var testConfig1 = &config.Config{
			*testGRPC,
			*testREST,
			*testLogger,
			*testMonitor,
			*testInspector,
			*testExtraAttributes,
			*testFilter1,
		}

		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig1)
		Client = mockHTTPClient

		jsonResponse := `{
			"1": {
				"ham": 0.01,
				"spam": 0.99
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_BLOCK

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with empty AL, empty DL", func(t *testing.T) {
		var testFilter1 = &config.Filter{
			AllowList: map[string]string{},
        	DenyList: map[string]string{},
		}

		var testConfig1 = &config.Config{
			*testGRPC,
			*testREST,
			*testLogger,
			*testMonitor,
			*testInspector,
			*testExtraAttributes,
			*testFilter1,
		}

		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig1)
		Client = mockHTTPClient

		jsonResponse := `{
			"1": {
				"ham": 0.01,
				"spam": 0.99
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		resp := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}
		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&resp, nil)

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project1,
		}

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}
		want := spamcheck.SpamVerdict_BLOCK

		assert.Equal(t, got.Verdict, want, "Got %s want %s")
	})

	t.Run("testing with error Posting to Inspector", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		mockResponse := http.Response{
			StatusCode: 500,
		}

		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&mockResponse, errors.New("Error posting"))

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}

		expected := spamcheck.SpamVerdict_NOOP

		assert.Equal(t, got.Verdict, expected, "Got %s want %s")
	})

	t.Run("testing returns error with verdict when Posting to Inspector fails", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient

		issue := spamcheck.Issue{
			User:          &user,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		mockResponse := http.Response{
			StatusCode: 500,
		}

		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&mockResponse, errors.New("Error posting"))

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}

		expected := "unexpected end of JSON input"

		assert.Equal(t, got.Error, expected, "Got %s want %s")
	})

	t.Run("testing returns ALLOW when User is a GitLab employee", func(t *testing.T) {
		mockHTTPClient := new(MockHTTPClient)
		controller := CreateSpamCheckController(testConfig)
		Client = mockHTTPClient

		issue := spamcheck.Issue{
			User:          &gitlab_employee,
			Title:         "Test title",
			Description:   "test description",
			UserInProject: true,
			CreatedAt:     ptypes.TimestampNow(),
			Project:       &project,
		}

		// This is never used, it's just here to show that GitLab employees' issues will pass
		jsonResponse := `{
			"1": {
				"ham": 0.0,
				"spam": 1.0
			}}`

		respBody := ioutil.NopCloser(strings.NewReader(jsonResponse))

		mockResponse := http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.0",
			Body:       respBody,
		}

		mockHTTPClient.On("Post", "fake.url", "application/json", mock.Anything).Return(&mockResponse, nil)

		got, err := controller.CheckForSpamIssue(context.Background(), &issue)
		if err != nil {
			assert.Failf(t, "Unexpected error", "Error when checking for spam %s", err)
		}

		expected := spamcheck.SpamVerdict_ALLOW

		assert.Equal(t, got.Verdict, expected, "Got %s want %s")
	})
}
