package app

import (
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"

	"github.com/mattermost/focalboard/server/model"
)

type contentOrderMatcher struct {
	contentOrder []string
}

func NewContentOrderMatcher(contentOrder []string) contentOrderMatcher {
	return contentOrderMatcher{contentOrder}
}

func (com contentOrderMatcher) Matches(x interface{}) bool {
	patch, ok := x.(*model.BlockPatch)
	if !ok {
		return false
	}

	contentOrderData, ok := patch.UpdatedFields["contentOrder"]
	if !ok {
		return false
	}

	contentOrder, ok := contentOrderData.([]interface{})
	if !ok {
		return false
	}

	if len(contentOrder) != len(com.contentOrder) {
		return false
	}

	for i := range contentOrder {
		if contentOrder[i] != com.contentOrder[i] {
			return false
		}
	}
	return true
}

func (com contentOrderMatcher) String() string {
	return fmt.Sprint(&model.BlockPatch{UpdatedFields: map[string]interface{}{"contentOrder": com.contentOrder}})
}

func TestMoveContentBlock(t *testing.T) {
	th, tearDown := SetupTestHelper(t)
	defer tearDown()

	ttCases := []struct {
		name                 string
		srcBlock             model.Block
		dstBlock             model.Block
		parentBlock          *model.Block
		where                string
		userID               string
		mockPatch            bool
		mockPatchError       error
		errorMessage         string
		expectedContentOrder []string
	}{
		{
			name:         "not matching parents",
			srcBlock:     model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:     model.Block{ID: "test-2", ParentID: "other-test-card"},
			parentBlock:  nil,
			where:        "after",
			userID:       "user-id",
			errorMessage: "not matching parent test-card and other-test-card",
		},
		{
			name:         "parent not found",
			srcBlock:     model.Block{ID: "test-1", ParentID: "invalid-card"},
			dstBlock:     model.Block{ID: "test-2", ParentID: "invalid-card"},
			parentBlock:  &model.Block{ID: "invalid-card"},
			where:        "after",
			userID:       "user-id",
			errorMessage: "{test} not found",
		},
		{
			name:         "valid parent without content order",
			srcBlock:     model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:     model.Block{ID: "test-2", ParentID: "test-card"},
			parentBlock:  &model.Block{ID: "test-card"},
			where:        "after",
			userID:       "user-id",
			errorMessage: "source block test-1 not found",
		},
		{
			name:         "valid parent with content order but without test-1 in it",
			srcBlock:     model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:     model.Block{ID: "test-2", ParentID: "test-card"},
			parentBlock:  &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-2"}}},
			where:        "after",
			userID:       "user-id",
			errorMessage: "source block test-1 not found",
		},
		{
			name:         "valid parent with content order but without test-2 in it",
			srcBlock:     model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:     model.Block{ID: "test-2", ParentID: "test-card"},
			parentBlock:  &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1"}}},
			where:        "after",
			userID:       "user-id",
			errorMessage: "destination block test-2 not found",
		},
		{
			name:           "valid request but fail on patchparent with content order",
			srcBlock:       model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:       model.Block{ID: "test-2", ParentID: "test-card"},
			parentBlock:    &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2"}}},
			where:          "after",
			userID:         "user-id",
			mockPatch:      true,
			mockPatchError: errors.New("test error"),
			errorMessage:   "test error",
		},
		{
			name:                 "valid request with not real change",
			srcBlock:             model.Block{ID: "test-2", ParentID: "test-card"},
			dstBlock:             model.Block{ID: "test-1", ParentID: "test-card"},
			parentBlock:          &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"},
			where:                "after",
			userID:               "user-id",
			mockPatch:            true,
			errorMessage:         "",
			expectedContentOrder: []string{"test-1", "test-2", "test-3"},
		},
		{
			name:                 "valid request changing order with before",
			srcBlock:             model.Block{ID: "test-2", ParentID: "test-card"},
			dstBlock:             model.Block{ID: "test-1", ParentID: "test-card"},
			parentBlock:          &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"},
			where:                "before",
			userID:               "user-id",
			mockPatch:            true,
			errorMessage:         "",
			expectedContentOrder: []string{"test-2", "test-1", "test-3"},
		},
		{
			name:                 "valid request changing order with after",
			srcBlock:             model.Block{ID: "test-1", ParentID: "test-card"},
			dstBlock:             model.Block{ID: "test-2", ParentID: "test-card"},
			parentBlock:          &model.Block{ID: "test-card", Fields: map[string]interface{}{"contentOrder": []interface{}{"test-1", "test-2", "test-3"}}, BoardID: "test-board"},
			where:                "after",
			userID:               "user-id",
			mockPatch:            true,
			errorMessage:         "",
			expectedContentOrder: []string{"test-2", "test-1", "test-3"},
		},
	}

	for _, tc := range ttCases {
		t.Run(tc.name, func(t *testing.T) {
			tc := tc
			if tc.parentBlock != nil {
				if tc.parentBlock.ID == "invalid-card" {
					th.Store.EXPECT().GetBlock(tc.srcBlock.ParentID).Return(nil, model.NewErrNotFound("test"))
				} else {
					th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil)
					if tc.mockPatch {
						if tc.mockPatchError != nil {
							th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(nil, tc.mockPatchError)
						} else {
							th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil)
							th.Store.EXPECT().PatchBlock(tc.parentBlock.ID, NewContentOrderMatcher(tc.expectedContentOrder), gomock.Eq("user-id")).Return(nil)
							th.Store.EXPECT().GetBlock(tc.parentBlock.ID).Return(tc.parentBlock, nil)
							th.Store.EXPECT().GetBoard(tc.parentBlock.BoardID).Return(&model.Board{ID: "test-board"}, nil)
							// this call comes from the WS server notification
							th.Store.EXPECT().GetMembersForBoard(gomock.Any()).Times(1)
						}
					}
				}
			}

			err := th.App.MoveContentBlock(&tc.srcBlock, &tc.dstBlock, tc.where, tc.userID)
			if tc.errorMessage == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, tc.errorMessage)
			}
		})
	}
}
