/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package route

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"testing"

	"github.com/shiningrush/droplet"
	"github.com/shiningrush/droplet/data"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"github.com/apisix/manager-api/internal/core/entity"
	"github.com/apisix/manager-api/internal/core/store"
	"github.com/apisix/manager-api/internal/handler"
	"github.com/apisix/manager-api/internal/utils/consts"
)

type testCase struct {
	caseDesc     string
	giveInput    interface{}
	mockInput    interface{}
	mockRet      interface{}
	mockErr      interface{}
	wantRet      interface{}
	wantErr      interface{}
	called       bool
	serviceInput string
	scriptRet    interface{}
	scriptErr    error
	serviceRet   interface{}
	serviceErr   error
	upstreamRet  interface{}
	upstreamErr  error
	nameExistRet []interface{}
}

var DagScript = `
{
    "rule":{
        "root":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
        "451106f8-560c-43a4-acf2-2a6ed0ea57b8":[
            [
                "code == 403",
                "b93d622c-92ef-48b4-b6bb-57e1ce893ee3"
            ],
            [
                "",
                "988ef5c2-c896-4606-a666-3d4cbe24a731"
            ]
        ]
    },
    "conf":{
        "451106f8-560c-43a4-acf2-2a6ed0ea57b8":{
            "name":"uri-blocker",
            "conf":{
                "block_rules":[
                    "root.exe",
                    "root.m+"
                ],
                "rejected_code":403
            }
        },
        "988ef5c2-c896-4606-a666-3d4cbe24a731":{
            "name":"kafka-logger",
            "conf":{
                "batch_max_size":1000,
                "broker_list":{

                },
                "buffer_duration":60,
                "inactive_timeout":5,
                "include_req_body":false,
                "kafka_topic":"1",
                "key":"2",
                "max_retry_count":0,
                "name":"kafka logger",
                "retry_delay":1,
                "timeout":3
            }
        },
        "b93d622c-92ef-48b4-b6bb-57e1ce893ee3":{
            "name":"fault-injection",
            "conf":{
                "abort":{
                    "body":"200",
                    "http_status":300
                },
                "delay":{
                    "duration":500
                }
            }
        }
    },
    "chart":{
        "hovered":{

        },
        "links":{
            "3a110c30-d6f3-40b1-a8ac-b828cfaa2489":{
                "from":{
                    "nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
                    "portId":"port3"
                },
                "id":"3a110c30-d6f3-40b1-a8ac-b828cfaa2489",
                "to":{
                    "nodeId":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
                    "portId":"port1"
                }
            },
            "c1958993-c1ef-44b1-bb32-7fc6f34870c2":{
                "from":{
                    "nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
                    "portId":"port2"
                },
                "id":"c1958993-c1ef-44b1-bb32-7fc6f34870c2",
                "to":{
                    "nodeId":"988ef5c2-c896-4606-a666-3d4cbe24a731",
                    "portId":"port1"
                }
            },
            "f9c42bf6-c8aa-4e86-8498-8dfbc5c53c23":{
                "from":{
                    "nodeId":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
                    "portId":"port2"
                },
                "id":"f9c42bf6-c8aa-4e86-8498-8dfbc5c53c23",
                "to":{
                    "nodeId":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
                    "portId":"port1"
                }
            }
        },
        "nodes":{
            "3365eca3-4bc8-4769-bab3-1485dfd6a43c":{
                "id":"3365eca3-4bc8-4769-bab3-1485dfd6a43c",
                "orientation":0,
                "ports":{
                    "port1":{
                        "id":"port1",
                        "position":{
                            "x":107,
                            "y":0
                        },
                        "type":"input"
                    },
                    "port2":{
                        "id":"port2",
                        "position":{
                            "x":92,
                            "y":96
                        },
                        "properties":{
                            "value":"no"
                        },
                        "type":"output"
                    },
                    "port3":{
                        "id":"port3",
                        "position":{
                            "x":122,
                            "y":96
                        },
                        "properties":{
                            "value":"yes"
                        },
                        "type":"output"
                    }
                },
                "position":{
                    "x":750.2627969928922,
                    "y":301.0370335799397
                },
                "properties":{
                    "customData":{
                        "name":"code == 403",
                        "type":1
                    }
                },
                "size":{
                    "height":96,
                    "width":214
                },
                "type":"判断条件"
            },
            "451106f8-560c-43a4-acf2-2a6ed0ea57b8":{
                "id":"451106f8-560c-43a4-acf2-2a6ed0ea57b8",
                "orientation":0,
                "ports":{
                    "port1":{
                        "id":"port1",
                        "position":{
                            "x":100,
                            "y":0
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"input"
                    },
                    "port2":{
                        "id":"port2",
                        "position":{
                            "x":100,
                            "y":96
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"output"
                    }
                },
                "position":{
                    "x":741.5684544145346,
                    "y":126.75879247285502
                },
                "properties":{
                    "customData":{
                        "data":{
                            "block_rules":[
                                "root.exe",
                                "root.m+"
                            ],
                            "rejected_code":403
                        },
                        "name":"uri-blocker",
                        "type":0
                    }
                },
                "size":{
                    "height":96,
                    "width":201
                },
                "type":"uri-blocker"
            },
            "988ef5c2-c896-4606-a666-3d4cbe24a731":{
                "id":"988ef5c2-c896-4606-a666-3d4cbe24a731",
                "orientation":0,
                "ports":{
                    "port1":{
                        "id":"port1",
                        "position":{
                            "x":106,
                            "y":0
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"input"
                    },
                    "port2":{
                        "id":"port2",
                        "position":{
                            "x":106,
                            "y":96
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"output"
                    }
                },
                "position":{
                    "x":607.9687500000001,
                    "y":471.17788461538447
                },
                "properties":{
                    "customData":{
                        "data":{
                            "batch_max_size":1000,
                            "broker_list":{

                            },
                            "buffer_duration":60,
                            "inactive_timeout":5,
                            "include_req_body":false,
                            "kafka_topic":"1",
                            "key":"2",
                            "max_retry_count":0,
                            "name":"kafka logger",
                            "retry_delay":1,
                            "timeout":3
                        },
                        "name":"kafka-logger",
                        "type":0
                    }
                },
                "size":{
                    "height":96,
                    "width":212
                },
                "type":"kafka-logger"
            },
            "b93d622c-92ef-48b4-b6bb-57e1ce893ee3":{
                "id":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
                "orientation":0,
                "ports":{
                    "port1":{
                        "id":"port1",
                        "position":{
                            "x":110,
                            "y":0
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"input"
                    },
                    "port2":{
                        "id":"port2",
                        "position":{
                            "x":110,
                            "y":96
                        },
                        "properties":{
                            "custom":"property"
                        },
                        "type":"output"
                    }
                },
                "position":{
                    "x":988.9074986362261,
                    "y":478.62041800736495
                },
                "properties":{
                    "customData":{
                        "data":{
                            "abort":{
                                "body":"200",
                                "http_status":300
                            },
                            "delay":{
                                "duration":500
                            }
                        },
                        "name":"fault-injection",
                        "type":0
                    }
                },
                "size":{
                    "height":96,
                    "width":219
                },
                "type":"fault-injection"
            }
        },
        "offset":{
            "x":-376.83,
            "y":87.98
        },
        "scale":0.832,
        "selected":{
            "id":"b93d622c-92ef-48b4-b6bb-57e1ce893ee3",
            "type":"node"
        }
    }
}`

func TestRoute_Get(t *testing.T) {
	tests := []testCase{
		{
			caseDesc:  "route: get success",
			giveInput: &GetInput{ID: "r1"},
			mockInput: "r1",
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				URI: "/test",
			},
			mockErr: nil,
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Script: "script",
				URI:    "/test",
			},
			wantErr:   nil,
			called:    true,
			scriptRet: &entity.Script{ID: "r1", Script: "script"},
			scriptErr: nil,
		},
		{
			caseDesc:  "route: store get failed",
			giveInput: &GetInput{ID: "failed_key"},
			mockInput: "failed_key",
			mockRet:   nil,
			mockErr:   fmt.Errorf("get failed"),
			wantRet: &data.SpecCodeResponse{
				StatusCode: http.StatusNotFound,
			},
			wantErr: fmt.Errorf("get failed"),
			called:  true,
		},
		{
			caseDesc:  "script: store get failed",
			giveInput: &GetInput{ID: "failed_key"},
			mockInput: "failed_key",
			mockRet:   nil,
			mockErr:   fmt.Errorf("get failed"),
			wantRet: &data.SpecCodeResponse{
				StatusCode: http.StatusNotFound,
			},
			wantErr:   fmt.Errorf("get failed"),
			scriptErr: errors.New("get failed"),
			called:    true,
		},
	}

	for _, tc := range tests {
		getCalled := false
		mStore := &store.MockInterface{}

		mStore.On("Get", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			getCalled = true
			assert.Equal(t, tc.mockInput, args.Get(0))
		}).Return(tc.mockRet, tc.mockErr)

		sStore := &store.MockInterface{}
		sStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)

		h := Handler{routeStore: mStore, scriptStore: sStore}
		ctx := droplet.NewContext()
		ctx.SetInput(tc.giveInput)
		ret, err := h.Get(ctx)
		assert.True(t, tc.called, getCalled)
		assert.Equal(t, tc.wantRet, ret)
		assert.Equal(t, tc.wantErr, err)
	}
}

func TestRoute_List(t *testing.T) {
	mockData := []*entity.Route{
		{
			BaseInfo: entity.BaseInfo{CreateTime: 1609742634},
			Name:     "r1",
			URI:      "/test_r1",
			Labels: map[string]string{
				"version": "v1",
				"build":   "16",
			},
			Upstream: &entity.UpstreamDef{
				Nodes: []interface{}{
					map[string]interface{}{
						"host":     "39.97.63.215",
						"port":     float64(80),
						"weight":   float64(1),
						"priority": float64(10),
					},
				},
			},
		},
		{
			BaseInfo: entity.BaseInfo{CreateTime: 1609742635},
			Name:     "r2",
			URI:      "/test_r2",
			Labels: map[string]string{
				"version": "v1",
				"build":   "16",
			},
		},
		{
			BaseInfo: entity.BaseInfo{CreateTime: 1609742636},
			Name:     "route_test",
			URI:      "/test_route_test",
			Labels: map[string]string{
				"version": "v2",
				"build":   "17",
			},
		},
		{
			BaseInfo: entity.BaseInfo{CreateTime: 1609742636},
			Name:     "test_route",
			URI:      "/test_test_route",
			Labels: map[string]string{
				"version": "v2",
				"build":   "17",
				"extra":   "test",
			},
		},
	}

	tests := []testCase{
		{
			caseDesc: "list all route",
			giveInput: &ListInput{
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					mockData[0],
					mockData[1],
					mockData[2],
					mockData[3],
				},
				TotalSize: 4,
			},
			scriptRet: &entity.Script{ID: "r1", Script: "script"},
			called:    true,
		},
		{
			caseDesc: "list routes with name",
			giveInput: &ListInput{
				Name: "route",
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					mockData[2],
					mockData[3],
				},
				TotalSize: 2,
			},
			scriptRet: &entity.Script{ID: "r1", Script: "script"},
			called:    true,
		},
		{
			caseDesc: "list routes with uri",
			giveInput: &ListInput{
				URI: "test_r2",
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					mockData[1],
				},
				TotalSize: 1,
			},
			scriptRet: &entity.Script{ID: "r1", Script: "script"},
			called:    true,
		},
		{
			caseDesc: "list routes with label",
			giveInput: &ListInput{
				Label: "version:v1",
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					mockData[0],
					mockData[1],
				},
				TotalSize: 2,
			},
			scriptRet: &entity.Script{ID: "s1", Script: "script"},
			called:    true,
		},
		{
			caseDesc: "list routes with label",
			giveInput: &ListInput{
				Label: "extra",
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					mockData[3],
				},
				TotalSize: 1,
			},
			scriptRet: &entity.Script{ID: "s1", Script: "script"},
			called:    true,
		},
		{
			caseDesc: "list routes and test format",
			giveInput: &ListInput{
				Name: "r1",
				Pagination: store.Pagination{
					PageSize:   10,
					PageNumber: 10,
				},
			},
			mockInput: store.ListInput{
				PageSize:   10,
				PageNumber: 10,
			},
			wantRet: &store.ListOutput{
				Rows: []interface{}{
					&entity.Route{
						BaseInfo: entity.BaseInfo{CreateTime: 1609742634},
						Name:     "r1",
						URI:      "/test_r1",
						Labels: map[string]string{
							"version": "v1",
							"build":   "16",
						},
						Script: "script",
						Upstream: &entity.UpstreamDef{
							Nodes: []*entity.Node{
								{
									Host:     "39.97.63.215",
									Port:     80,
									Weight:   1,
									Priority: 10,
								},
							},
						},
					},
				},

				TotalSize: 1,
			},
			called: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false
			mStore := &store.MockInterface{}
			mStore.On("List", mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
				input := args.Get(0).(store.ListInput)
				mockInput := tc.mockInput.(store.ListInput)
				assert.Equal(t, mockInput.PageSize, input.PageSize)
				assert.Equal(t, mockInput.PageNumber, input.PageNumber)
			}).Return(func(input store.ListInput) *store.ListOutput {
				var returnData []interface{}
				for _, c := range mockData {
					if input.Predicate(c) {
						if input.Format == nil {
							returnData = append(returnData, c)
							continue
						}

						returnData = append(returnData, input.Format(c))
					}
				}

				return &store.ListOutput{
					Rows:      returnData,
					TotalSize: len(returnData),
				}
			}, tc.mockErr)

			sStore := &store.MockInterface{}
			sStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)

			h := Handler{routeStore: mStore, scriptStore: sStore}
			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)

			ret, err := h.List(ctx)
			assert.Equal(t, tc.called, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			assert.Equal(t, tc.wantErr, err)
		})
	}
}

func TestRoute_Create(t *testing.T) {
	scriptMap := make(map[string]interface{})

	err := json.Unmarshal([]byte(DagScript), &scriptMap)
	assert.Nil(t, err)

	luaCode, err := generateLuaCode(scriptMap)
	assert.Nil(t, err)

	tests := []testCase{
		{
			caseDesc: "create route success",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				ScriptID:   "s1",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				ScriptID:   "s1",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			serviceRet:  "service",
			serviceErr:  nil,
			upstreamRet: "upstream",
			upstreamErr: nil,
			scriptRet:   "script",
			scriptErr:   nil,
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				ScriptID:   "s1",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantErr: nil,
			called:  true,
		},
		{
			caseDesc: "create route failed, service not found",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s2",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "not_found",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantRet:    &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:    fmt.Errorf("service id: not_found not found"),
			serviceRet: nil,
			serviceErr: data.ErrNotFound,
		},
		{
			caseDesc: "create route failed, service store get error",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609746531,
				},
				Name:       "r1",
				Desc:       "test route",
				UpstreamID: "r1",
				// mock store will return err if service is s3
				ServiceID: "error",
				Script:    "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantRet:    &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:    errors.New("service error"),
			serviceRet: nil,
			serviceErr: errors.New("service error"),
			called:     false,
		},
		{
			caseDesc: "create route failed, upstream not found",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s2",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "not_found",
				ServiceID:  "s2",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantRet:     &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:     fmt.Errorf("upstream id: not_found not found"),
			upstreamErr: data.ErrNotFound,
			called:      false,
		},
		{
			caseDesc: "create route failed, upstream store get error",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s2",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "error",
				ServiceID:  "s2",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantRet:     &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:     errors.New("upstream error"),
			upstreamErr: errors.New("upstream error"),
			called:      false,
		},
		{
			caseDesc: "create route failed, script create error",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s2",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s2",
				Script:     "",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantRet:    &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:    fmt.Errorf("upstream error"),
			serviceErr: fmt.Errorf("upstream error"),
			called:     false,
		},
		{
			caseDesc: "create route success with script",
			giveInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				Script:     scriptMap,
				Labels: map[string]string{
					"version": "v1",
				},
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				Script:     luaCode,
				ScriptID:   "s1",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				Script:     luaCode,
				ScriptID:   "s1",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			serviceRet:  "service",
			serviceErr:  nil,
			upstreamRet: "upstream",
			upstreamErr: nil,
			scriptRet:   "script",
			scriptErr:   nil,
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "s1",
					CreateTime: 1609746531,
				},
				Name:       "s1",
				Desc:       "test_route",
				UpstreamID: "u1",
				ServiceID:  "s1",
				Script:     luaCode,
				ScriptID:   "s1",
				Labels: map[string]string{
					"version": "v1",
				},
			},
			wantErr: nil,
			called:  true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false

			mStore := &store.MockInterface{}
			mStore.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
				route := args.Get(1).(*entity.Route)
				assert.Equal(t, tc.mockInput, route)
			}).Return(tc.mockRet, tc.mockErr)

			mStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			}).Return(func(input store.ListInput) *store.ListOutput {
				return &store.ListOutput{
					Rows:      tc.nameExistRet,
					TotalSize: len(tc.nameExistRet),
				}
			}, nil)

			svcStore := &store.MockInterface{}
			svcStore.On("Get", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.serviceErr)

			uStore := &store.MockInterface{}
			uStore.On("Get", mock.Anything, mock.Anything).Return(tc.upstreamRet, tc.upstreamErr)

			scriptStore := &store.MockInterface{}
			scriptStore.On("Create", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.serviceErr)

			h := Handler{routeStore: mStore, svcStore: svcStore, upstreamStore: uStore, scriptStore: scriptStore}

			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)
			ret, err := h.Create(ctx)
			assert.Equal(t, tc.called, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			assert.Equal(t, tc.wantErr, err)
		})
	}
}

func TestRoute_Update(t *testing.T) {
	luaScript := "local _M = {} \n function _M.access(api_ctx) \n ngx.log(ngx.WARN,\"hit access phase\") \n end \nreturn _M"
	scriptMap := make(map[string]interface{})
	err := json.Unmarshal([]byte(DagScript), &scriptMap)
	assert.Nil(t, err)

	luaCode, err := generateLuaCode(scriptMap)
	assert.Nil(t, err)
	tests := []testCase{
		{
			caseDesc: "update success with script",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					Name:       "r1",
					Desc:       "updated route",
					UpstreamID: "u2",
					Script:     luaScript,
					ServiceID:  "s1",
					Labels: map[string]string{
						"version": "v2",
					},
				},
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaScript,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaScript,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaScript,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			mockErr:      nil,
			serviceInput: "s2",
			called:       true,
		},
		{
			caseDesc: "update success with script map",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					Name:       "r1",
					Desc:       "updated route",
					UpstreamID: "u2",
					Script:     scriptMap,
					ServiceID:  "s1",
					Labels: map[string]string{
						"version": "v2",
					},
				},
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaCode,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaCode,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			mockErr: nil,
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name:       "r1",
				Desc:       "updated route",
				UpstreamID: "u2",
				Script:     luaCode,
				ScriptID:   "r1",
				ServiceID:  "s1",
				Labels: map[string]string{
					"version": "v2",
				},
			},
			serviceInput: "s2",
			called:       true,
		},
		{
			caseDesc: "update failed, different id",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					BaseInfo: entity.BaseInfo{
						ID: "r2",
					},
					Name:       "test_route",
					UpstreamID: "u1",
					Desc:       "test service",
				},
			},
			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr: fmt.Errorf("ID on path (r1) doesn't match ID on body (r2)"),
			called:  false,
		},
		{
			caseDesc: "update failed, service not found",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					BaseInfo: entity.BaseInfo{
						ID: "r1",
					},
					Name:       "test route",
					ServiceID:  "not_found",
					UpstreamID: "u1",
				},
			},
			wantRet:    &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:    fmt.Errorf("service id: not_found not found"),
			serviceErr: data.ErrNotFound,
			called:     false,
		},
		{
			caseDesc: "update failed, upstream not found",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					Name:       "test_route",
					UpstreamID: "not_found",
					ServiceID:  "s1",
					Desc:       "test route",
				},
			},
			wantRet:     &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr:     fmt.Errorf("upstream id: not_found not found"),
			serviceRet:  "service",
			upstreamErr: data.ErrNotFound,
			called:      false,
		},
		{
			caseDesc: "update failed, route return error",
			giveInput: &UpdateInput{
				ID: "r1",
				Route: entity.Route{
					Name: "r1",
					Desc: "test route",
				},
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID: "r1",
				},
				Name: "r1",
				Desc: "test route",
			},
			mockErr:      fmt.Errorf("route update error"),
			wantErr:      fmt.Errorf("route update error"),
			wantRet:      &data.SpecCodeResponse{StatusCode: http.StatusInternalServerError},
			serviceRet:   "service",
			upstreamRet:  "upstream",
			serviceInput: "s1",
			called:       true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false
			routeStore := &store.MockInterface{}

			routeStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
				input := args.Get(1).(*entity.Route)
				createIfNotExist := args.Get(2).(bool)
				assert.Equal(t, tc.mockInput, input)
				assert.True(t, createIfNotExist)
			}).Return(tc.mockRet, tc.mockErr)

			routeStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			}).Return(func(input store.ListInput) *store.ListOutput {
				return &store.ListOutput{
					Rows:      tc.nameExistRet,
					TotalSize: len(tc.nameExistRet),
				}
			}, nil)

			serviceStore := &store.MockInterface{}
			serviceStore.On("Get", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.serviceErr)

			upstreamStore := &store.MockInterface{}
			upstreamStore.On("Get", mock.Anything, mock.Anything).Return(tc.upstreamRet, tc.upstreamErr)

			scriptStore := &store.MockInterface{}
			scriptStore.On("Get", mock.Anything, mock.Anything).Return(tc.scriptRet, tc.scriptErr)
			scriptStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(luaScript, nil)

			h := Handler{svcStore: serviceStore, upstreamStore: upstreamStore, scriptStore: scriptStore,
				routeStore: routeStore}

			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)
			ret, err := h.Update(ctx)
			assert.Equal(t, tc.called, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			assert.Equal(t, tc.wantErr, err)
		})
	}
}

func TestRoute_Patch(t *testing.T) {
	existRoute := &entity.Route{
		BaseInfo: entity.BaseInfo{
			ID:         "r1",
			CreateTime: 1609340491,
			UpdateTime: 1609340491,
		},
		Name:       "exist_service",
		UpstreamID: "u1",
		Upstream: &entity.UpstreamDef{
			Key: "key",
		},
		EnableWebsocket: false,
		Labels: map[string]string{
			"version": "v1",
		},
		Plugins: map[string]interface{}{
			"limit-count": map[string]interface{}{
				"count":         2,
				"time_window":   60,
				"rejected_code": 503,
				"key":           "remote_addr",
			},
		},
		Status: 1,
	}

	tests := []testCase{
		{
			caseDesc: "patch success",
			giveInput: &PatchInput{
				ID:      "r1",
				SubPath: "",
				Body:    []byte("{\"status\":0}"),
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			called: true,
		},
		{
			caseDesc: "patch success by path",
			giveInput: &PatchInput{
				ID:      "r1",
				SubPath: "/status",
				Body:    []byte("0"),
			},
			mockInput: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			mockRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			wantRet: &entity.Route{
				BaseInfo: entity.BaseInfo{
					ID:         "r1",
					CreateTime: 1609340491,
					UpdateTime: 1609340491,
				},
				Name:       "exist_service",
				UpstreamID: "u1",
				Upstream: &entity.UpstreamDef{
					Key: "key",
				},
				EnableWebsocket: false,
				Labels: map[string]string{
					"version": "v1",
				},
				Plugins: map[string]interface{}{
					"limit-count": map[string]interface{}{
						"count":         float64(2),
						"time_window":   float64(60),
						"rejected_code": float64(503),
						"key":           "remote_addr",
					},
				},
				Status: 0,
			},
			called: true,
		},
		{
			caseDesc: "patch failed, path error",
			giveInput: &PatchInput{
				ID:      "r1",
				SubPath: "error",
				Body:    []byte("0"),
			},
			wantRet: handler.SpecCodeResponse(
				errors.New("add operation does not apply: doc is missing path: \"error\": missing value")),
			wantErr: errors.New("add operation does not apply: doc is missing path: \"error\": missing value"),
		},
	}
	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false

			routeStore := &store.MockInterface{}
			routeStore.On("Get", mock.Anything, mock.Anything).Return(existRoute, nil)
			routeStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
				input := args.Get(1).(*entity.Route)
				createIfNotExist := args.Get(2).(bool)
				assert.Equal(t, tc.mockInput, input)
				assert.False(t, createIfNotExist)
			}).Return(tc.mockRet, tc.mockErr)
			h := Handler{routeStore: routeStore}
			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)
			ret, err := h.Patch(ctx)
			assert.Equal(t, tc.called, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			if tc.wantErr != nil && err != nil {
				assert.Error(t, tc.wantErr.(error), err.Error())
			} else {
				assert.Equal(t, tc.wantErr, err)
			}
		})
	}
}

func TestRoute_Delete(t *testing.T) {
	tests := []testCase{
		{
			caseDesc: "delete success",
			giveInput: &BatchDelete{
				IDs: "r1",
			},
			mockInput: []string{"r1"},
			called:    true,
		},
		{
			caseDesc: "batch delete success",
			giveInput: &BatchDelete{
				IDs: "r1,r2",
			},
			mockInput: []string{"r1", "r2"},
			called:    true,
		},
		{
			caseDesc: "delete failed, route delete error",
			giveInput: &BatchDelete{
				IDs: "r1",
			},
			mockInput: []string{"r1"},
			mockErr:   fmt.Errorf("delete error"),
			wantRet:   handler.SpecCodeResponse(fmt.Errorf("delete error")),
			wantErr:   fmt.Errorf("delete error"),
			called:    true,
		},
		{
			caseDesc: "delete failed, script delete error",
			giveInput: &BatchDelete{
				IDs: "r1",
			},
			mockInput: []string{"r1"},
			scriptErr: fmt.Errorf("delete error"),
			wantRet:   nil,
			wantErr:   nil,
			called:    true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false
			routeStore := &store.MockInterface{}
			routeStore.On("BatchDelete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
				input := args.Get(1).([]string)
				assert.Equal(t, tc.mockInput, input)
			}).Return(tc.mockErr)

			scriptStore := &store.MockInterface{}
			scriptStore.On("BatchDelete", mock.Anything, mock.Anything).Return(tc.serviceRet, tc.scriptErr)

			h := Handler{routeStore: routeStore, scriptStore: scriptStore}
			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)
			ret, err := h.BatchDelete(ctx)
			assert.True(t, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			if tc.wantErr != nil && err != nil {
				assert.Error(t, tc.wantErr.(error), err.Error())
			} else {
				assert.Equal(t, tc.wantErr, err)
			}
		})
	}
}

func TestRoute_Exist(t *testing.T) {
	mockData := []*entity.Route{
		{
			BaseInfo: entity.BaseInfo{ID: "001", CreateTime: 1609742634},
			Name:     "r1",
			URI:      "/test_r1",
			Labels: map[string]string{
				"version": "v1",
				"build":   "16",
			},
			Upstream: &entity.UpstreamDef{
				Nodes: []interface{}{
					map[string]interface{}{
						"host":   "39.97.63.215",
						"port":   float64(80),
						"weight": float64(1),
					},
				},
			},
		},
		{
			BaseInfo: entity.BaseInfo{ID: "002", CreateTime: 1609742635},
			Name:     "r2",
			URI:      "/test_r2",
			Labels: map[string]string{
				"version": "v1",
				"build":   "16",
			},
		},
		{
			BaseInfo: entity.BaseInfo{ID: "003", CreateTime: 1609742636},
			Name:     "route_test",
			URI:      "/test_route_test",
			Labels: map[string]string{
				"version": "v2",
				"build":   "17",
			},
		},
	}

	tests := []testCase{
		{
			caseDesc: "check route exist, excluded",
			giveInput: &ExistCheckInput{
				Name:    "r1",
				Exclude: "001",
			},
			wantRet: nil,
			called:  true,
		},
		{
			caseDesc: "check route exist, not excluded",
			giveInput: &ExistCheckInput{
				Name:    "r1",
				Exclude: "002",
			},
			wantRet: &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			wantErr: consts.InvalidParam("Route name is reduplicate"),
			called:  true,
		},
		{
			caseDesc: "check route exist, not existed",
			giveInput: &ExistCheckInput{
				Name:    "r3",
				Exclude: "001",
			},
			wantRet: nil,
			wantErr: nil,
			called:  true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.caseDesc, func(t *testing.T) {
			getCalled := false
			routeStore := &store.MockInterface{}
			routeStore.On("List", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
				getCalled = true
			}).Return(func(input store.ListInput) *store.ListOutput {
				var res []interface{}
				for _, c := range mockData {
					if input.Predicate(c) {
						if input.Format != nil {
							res = append(res, input.Format(c))
						} else {
							res = append(res, c)
						}
					}
				}

				return &store.ListOutput{
					Rows:      res,
					TotalSize: len(res),
				}
			}, nil)

			h := Handler{routeStore: routeStore}
			ctx := droplet.NewContext()
			ctx.SetInput(tc.giveInput)
			ret, err := h.Exist(ctx)
			assert.True(t, getCalled)
			assert.Equal(t, tc.wantRet, ret)
			assert.Equal(t, tc.wantErr, err)
		})
	}
}
