/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>
*/

package routes

import (
	"bytes"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/cgrates/birpc"
	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/engine"
	"github.com/cgrates/cgrates/utils"
)

// func TestPopulateCostForRoutesConnRefused(t *testing.T) {
// 	cfg := config.NewDefaultCGRConfig()
// 	connMgr := engine.NewConnManager(cfg)
// 	fltrS := engine.NewFilterS(cfg, connMgr, nil)
// 	routes := map[string]*RouteWithWeight{
// 		"RW": {
// 			Route: &utils.Route{
// 				ID:             "local",
// 				RateProfileIDs: []string{"RP_LOCAL"},
// 			},
// 			Weight: 10,
// 		},
// 	}
// 	ev := &utils.CGREvent{
// 		Tenant: "cgrates.org",
// 		Event: map[string]any{
// 			utils.Usage: "10s",
// 		},
// 		APIOpts: map[string]any{
// 			utils.OptsRatesProfileIDs: []string{},
// 		},
// 	}
// 	extraOpts := &optsGetRoutes{}
// 	cfg.RouteSCfg().RateSConns = []string{"*localhost"}
// 	_, err := populateCostForRoutes(context.Background(), cfg, connMgr, fltrS, routes, ev, extraOpts)
// 	errExpect := "RATES_ERROR:dial tcp 127.0.0.1:2012: connect: connection refused"
// 	if err == nil || err.Error() != errExpect {
// 		t.Errorf("Expected %v\n but received %v", errExpect, err)
// 	}
// }

func TestLeastCostSorterSortRoutesErr(t *testing.T) {

	cfg := config.NewDefaultCGRConfig()
	cM := engine.NewConnManager(cfg)
	fltrS := engine.NewFilterS(cfg, cM, nil)
	lcs := NewLeastCostSorter(cfg, cM, fltrS)

	expErr := "MANDATORY_IE_MISSING: [connIDs]"
	if _, err := lcs.SortRoutes(context.Background(), "", map[string]*RouteWithWeight{}, &utils.CGREvent{}, &optsGetRoutes{}); err == nil || err.Error() != expErr {
		t.Errorf("Expected error <%+v>, received <%+v>", expErr, err)
	}

}

func TestLeastCostSorterSortRoutesOK(t *testing.T) {

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				return nil
			},
		},
	}

	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	lcs := NewLeastCostSorter(cfg, cM, fltrS)

	routeWW := map[string]*RouteWithWeight{
		"RW1": {
			Route: &utils.Route{
				ID:              "RouteId",
				RouteParameters: "RouteParam",
				Weights:         utils.DynamicWeights{{Weight: 1}},
				Blockers:        utils.DynamicBlockers{{Blocker: false}},
				AccountIDs:      []string{"AccId"},
				RateProfileIDs:  []string{"RateProfileId"},
				ResourceIDs:     []string{"ResourceId"},
				StatIDs:         []string{"StatId"},
			},
			Weight: 1,
		},
		"RW2": {
			Route: &utils.Route{
				ID:              "RouteId2",
				RouteParameters: "RouteParam2",
				Weights:         utils.DynamicWeights{{Weight: 10}},
				Blockers:        utils.DynamicBlockers{{Blocker: false}},
				AccountIDs:      []string{"AccId2"},
				RateProfileIDs:  []string{"RateProfileId2"},
				ResourceIDs:     []string{"ResourceId2"},
				StatIDs:         []string{"StatId2"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		APIOpts: map[string]any{
			utils.MetaUsage: 400,
		},
	}

	exp := &SortedRoutes{
		ProfileID: "profID1",
		Sorting:   utils.MetaLC,
		Routes: []*SortedRoute{
			{
				RouteID:         "RouteId2",
				RouteParameters: "RouteParam2",
				SortingData: map[string]any{
					utils.AccountIDs: []string{},
					utils.Cost:       nil,
					utils.Weight:     10,
				},
			},
			{
				RouteID:         "RouteId",
				RouteParameters: "RouteParam",
				SortingData: map[string]any{
					utils.AccountIDs: []string{},
					utils.Cost:       nil,
					utils.Weight:     1,
				},
			},
		},
	}
	if rcv, err := lcs.SortRoutes(context.Background(), "profID1", routeWW, ev, &optsGetRoutes{}); err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(utils.ToJSON(exp), utils.ToJSON(rcv)) {
		t.Errorf("Expecting \n<%+v>,\n received \n<%+v>", utils.ToJSON(exp), utils.ToJSON(rcv))
	}

}

func TestHightCostSorterSortRoutesErr(t *testing.T) {

	cfg := config.NewDefaultCGRConfig()
	cM := engine.NewConnManager(cfg)
	fltrS := engine.NewFilterS(cfg, cM, nil)
	hcs := NewHighestCostSorter(cfg, cM, fltrS)

	expErr := "MANDATORY_IE_MISSING: [connIDs]"
	if _, err := hcs.SortRoutes(context.Background(), "", map[string]*RouteWithWeight{}, &utils.CGREvent{}, &optsGetRoutes{}); err == nil || err.Error() != expErr {
		t.Errorf("Expected error <%+v>, received <%+v>", expErr, err)
	}

}

func TestHightCostSorterSortRoutesOK(t *testing.T) {

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				return nil
			},
		},
	}

	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	hcs := NewHighestCostSorter(cfg, cM, fltrS)

	routeWW := map[string]*RouteWithWeight{
		"RW1": {
			Route: &utils.Route{
				ID:              "RouteId",
				RouteParameters: "RouteParam",
				Weights:         utils.DynamicWeights{{Weight: 1}},
				Blockers:        utils.DynamicBlockers{{Blocker: false}},
				AccountIDs:      []string{"AccId"},
				RateProfileIDs:  []string{"RateProfileId"},
				ResourceIDs:     []string{"ResourceId"},
				StatIDs:         []string{"StatId"},
			},
			Weight: 10,
		},
		"RW2": {
			Route: &utils.Route{
				ID:              "RouteId2",
				RouteParameters: "RouteParam2",
				Weights:         utils.DynamicWeights{{Weight: 10}},
				Blockers:        utils.DynamicBlockers{{Blocker: false}},
				AccountIDs:      []string{"AccId2"},
				RateProfileIDs:  []string{"RateProfileId2"},
				ResourceIDs:     []string{"ResourceId2"},
				StatIDs:         []string{"StatId2"},
			},
			Weight:  1,
			blocker: true,
		},
	}
	ev := &utils.CGREvent{
		APIOpts: map[string]any{
			utils.MetaUsage: 400,
		},
	}

	exp := &SortedRoutes{
		ProfileID: "profID1",
		Sorting:   utils.MetaHC,
		Routes: []*SortedRoute{
			{
				RouteID:         "RouteId",
				RouteParameters: "RouteParam",
				SortingData: map[string]any{
					utils.AccountIDs: []string{},
					utils.Cost:       nil,
					utils.Weight:     10,
				},
			},
			{
				RouteID:         "RouteId2",
				RouteParameters: "RouteParam2",
				SortingData: map[string]any{
					utils.AccountIDs: []string{},
					utils.Cost:       nil,
					utils.Weight:     1,
					utils.Blocker:    true,
				},
			},
		},
	}
	if rcv, err := hcs.SortRoutes(context.Background(), "profID1", routeWW, ev, &optsGetRoutes{}); err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(utils.ToJSON(exp), utils.ToJSON(rcv)) {
		t.Errorf("Expecting \n<%+v>,\n received \n<%+v>", utils.ToJSON(exp), utils.ToJSON(rcv))
	}

}

func TestPopulateCostForRoutesGetDecimalBigOptsErr(t *testing.T) {

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	strOpts := []*config.DynamicInterfaceOpt{
		{
			FilterIDs: []string{"*string.invalid:filter"},
			Tenant:    "cgrates.org",
			Value:     "-1",
		},
	}
	dynOpts, _ := config.IfaceToDecimalBigDynamicOpts(strOpts)
	cfg.RouteSCfg().Opts.Usage = dynOpts
	cM := engine.NewConnManager(cfg)
	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:             "local",
				RateProfileIDs: []string{"RP_LOCAL"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{}

	experr := `inline parse error for string: <*string.invalid:filter>`
	_, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err == nil || err.Error() != experr {
		t.Errorf("Expected error \n<%v>\n but received \n<%v>", experr, err)
	}

}

func TestPopulateCostForRoutesMissingIdsErr(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}

	cM := engine.NewConnManager(cfg)
	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:             "local",
				RateProfileIDs: []string{},
				AccountIDs:     []string{},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{}

	experr := `MANDATORY_IE_MISSING: [RateProfileIDs or AccountIDs]`
	_, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err == nil || err.Error() != experr {
		t.Errorf("Expected error \n<%v>\n but received \n<%v>", experr, err)
	}

}

func TestPopulateCostForRoutesAccountSConnsIgnoreErr(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	var buf bytes.Buffer
	utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 7)

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				return utils.ErrNotImplemented
			},
		},
	}

	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:         "local",
				AccountIDs: []string{"accID1"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{
		ignoreErrors: true,
	}

	rcv, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(rcv, []*SortedRoute{}) {
		t.Errorf("Received <%+v>", rcv)
	}

	expected := "CGRateS <> [WARNING] <RouteS> ignoring route with ID: local, err: NOT_IMPLEMENTED"
	if rcv := buf.String(); !strings.Contains(rcv, expected) {
		t.Errorf("Expected <%+v>, received <%+v>", expected, rcv)
	}

}

func TestPopulateCostForRoutesAccountSConnsErr(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				return utils.ErrNotImplemented
			},
		},
	}

	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:         "local",
				AccountIDs: []string{"accID1"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{}

	expErr := "ACCOUNTS_ERROR:NOT_IMPLEMENTED"
	_, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err == nil || err.Error() != expErr {
		t.Errorf("Expected error \n<%v>\n but received \n<%v>", expErr, err)
	}

}
func TestPopulateCostForRoutesAccountCostOverMax(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	acntCost := &utils.EventCharges{
		Concretes: utils.NewDecimal(5, 0),
	}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				rplCast, canCast := reply.(*utils.EventCharges)
				if !canCast {
					t.Errorf("Wrong argument type : %T", reply)
					return nil
				}
				*rplCast = *acntCost
				return nil
			},
		},
	}
	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:         "local",
				AccountIDs: []string{"accID1"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{
		maxCost: 1,
	}

	rcv, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(rcv, []*SortedRoute{}) {
		t.Errorf("Expected \n<%v>,\n received \n<%v>", []*SortedRoute{}, rcv)
	}

}

func TestPopulateCostForRoutesAppendAccounts(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}
	cfg.RouteSCfg().AccountSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts)}

	acnt := &utils.Account{
		Tenant: "cgrates.org",
		ID:     "accID1",
		Balances: map[string]*utils.Balance{
			"ab1": {
				ID:    "ab1",
				Type:  utils.MetaAbstract,
				Units: utils.NewDecimal(int64(60*time.Second), 0),
				CostIncrements: []*utils.CostIncrement{
					{
						FixedFee:  utils.NewDecimal(1, 1),
						Increment: utils.NewDecimal(1, 0),
					},
				},
			},
		},
	}

	acntCost := &utils.EventCharges{
		Accounts: map[string]*utils.Account{
			"accID1": acnt,
		},
		Concretes: utils.NewDecimal(5, 0),
	}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.AccountSv1MaxAbstracts: func(ctx *context.Context, args, reply any) error {
				rplCast, canCast := reply.(*utils.EventCharges)
				if !canCast {
					t.Errorf("Wrong argument type : %T", reply)
					return nil
				}
				*rplCast = *acntCost
				return nil
			},
		},
	}
	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAccounts), utils.AccountSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:         "local",
				AccountIDs: []string{"accID1"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{}

	exp := []*SortedRoute{
		{
			RouteID:         "local",
			RouteParameters: "",
			SortingData: map[string]any{
				utils.AccountIDs: []string{"accID1"},
				utils.Cost:       5,
				utils.Weight:     10,
			},
		},
	}

	rcv, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(utils.ToJSON(rcv), utils.ToJSON(exp)) {
		t.Errorf("Expected \n<%v>,\n received \n<%v>", utils.ToJSON(exp), utils.ToJSON(rcv))
	}

}

func TestPopulateCostForRoutesRateSIgnoreErr(t *testing.T) {

	defer func() {
		engine.Cache = engine.NewCacheS(config.NewDefaultCGRConfig(), nil, nil, nil)
	}()

	cfg := config.NewDefaultCGRConfig()
	cfg.RouteSCfg().RateSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates)}

	cc := make(chan birpc.ClientConnector, 1)
	cc <- &ccMock{

		calls: map[string]func(ctx *context.Context, args any, reply any) error{
			utils.RateSv1CostForEvent: func(ctx *context.Context, args, reply any) error {
				return utils.ErrNotImplemented
			},
		},
	}
	cM := engine.NewConnManager(cfg)
	cM.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaRates), utils.RateSv1, cc)

	fltrS := engine.NewFilterS(cfg, cM, nil)
	routes := map[string]*RouteWithWeight{
		"RW": {
			Route: &utils.Route{
				ID:             "local",
				RateProfileIDs: []string{"RPID1"},
			},
			Weight: 10,
		},
	}
	ev := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "TestEvent",
		Event: map[string]any{
			utils.AccountField: 1001,
		},
		APIOpts: map[string]any{},
	}
	extraOpts := &optsGetRoutes{
		ignoreErrors: true,
	}

	exp := []*SortedRoute{}

	rcv, err := populateCostForRoutes(context.Background(), cfg, cM, fltrS, routes, ev, extraOpts)
	if err != nil {
		t.Error(err)
	} else if !reflect.DeepEqual(utils.ToJSON(rcv), utils.ToJSON(exp)) {
		t.Errorf("Expected \n<%v>,\n received \n<%v>", utils.ToJSON(exp), utils.ToJSON(rcv))
	}

}
