//go:build integration
// +build integration

/*
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 general_tests

import (
	"fmt"
	"reflect"
	"sort"
	"testing"
	"time"

	"github.com/cgrates/birpc"
	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/analyzers"
	"github.com/cgrates/cgrates/attributes"
	"github.com/cgrates/cgrates/engine"
	"github.com/cgrates/cgrates/utils"
)

func TestAnzDocIT(t *testing.T) {
	switch *utils.DBType {
	case utils.MetaInternal:
	case utils.MetaMySQL, utils.MetaRedis, utils.MetaMongo, utils.MetaPostgres:
		t.SkipNow()
	default:
		t.Fatal("unsupported dbtype value")
	}
	jsonCfg := fmt.Sprintf(`{
"admins": {
	"enabled": true
},
"db": {
	"db_conns": {
		"*default": {
			"db_type": "*internal",
			"opts":{
			"internalDBRewriteInterval": "0s",
			"internalDBDumpInterval": "0s"
		}
    	}
	},
},
"attributes": {
	"enabled": true,
	"prefix_indexed_fields": ["*req.Destination", "*req.Account"],
	"opts": {
		"*processRuns": [{
			"Tenant": "cgrates.org",
			"FilterIDs": [],
			"Value": "2"
		}]
	}
},
"analyzers": {
	"enabled": true,
	"db_path": "%s"
}
}`, t.TempDir())
	ng := engine.TestEngine{
		ConfigJSON: jsonCfg,
		DBCfg:      engine.InternalDBCfg,
		Encoding:   *utils.Encoding,
	}
	client, _ := ng.Run(t)

	status := func(t *testing.T) {
		t.Helper()
		var status map[string]any
		if err := client.Call(context.Background(), utils.CoreSv1Status, utils.TenantWithAPIOpts{}, &status); err != nil {
			t.Error(err)
		}
	}

	//Generate traffic.
	status(t) //1*CoreSv1.Status

	time.Sleep(time.Second)
	timeVar := time.Now().UTC().Format("2006-01-02T15:04:05Z07:00")

	setAttrProfiles(t, client)   // 5*AdminSv1.SetAttributeProfiles
	getAttrProfiles(t, client)   // 1*AdminSv1.GetAttributeProfiles
	attrProcessEvents(t, client) // 3*AttributeSv1.ProcessEvent
	status(t)                    //1*CoreSv1.Status (2 total)

	time.Sleep(100 * time.Millisecond)

	// HeaderFilters only queries.
	anzStringQuery(t, client, 5, `+RequestMethod:"AdminSv1.SetAttributeProfile"`)
	anzStringQuery(t, client, 1, `+RequestMethod:"AdminSv1.GetAttributeProfiles"`)
	anzStringQuery(t, client, 3, `+RequestMethod:"AttributeSv1.ProcessEvent"`)
	anzStringQuery(t, client, 2, `+RequestMethod:"CoreSv1.Status"`)
	anzStringQuery(t, client, 5, `+RequestMethod:"CacheSv1.ReloadCache"`)
	anzStringQuery(t, client, 11, `-RequestMethod:"CacheSv1.ReloadCache"`)
	anzStringQuery(t, client, 6, `+RequestMethod:"/AdminSv1.*/"`) // regex

	// Query results that happen before a given time.
	headerFltr := `+RequestStartTime:<"%s"`
	headerFltr = fmt.Sprintf(headerFltr, timeVar)
	anzStringQuery(t, client, 1, headerFltr)

	// ContentFilters only queries.
	anzStringQuery(t, client, 2, `+RequestID:<=2 -RequestMethod:"CacheSv1.ReloadCache"`)
	anzStringQuery(t, client, 5, "", "*string:~*hdr.RequestMethod:AdminSv1.SetAttributeProfile")
	anzStringQuery(t, client, 1, "", "*string:~*hdr.RequestMethod:AdminSv1.GetAttributeProfiles")
	anzStringQuery(t, client, 3, "", "*string:~*hdr.RequestMethod:AttributeSv1.ProcessEvent")
	anzStringQuery(t, client, 2, "", "*string:~*hdr.RequestMethod:CoreSv1.Status")
	anzStringQuery(t, client, 5, "", "*string:~*hdr.RequestMethod:CacheSv1.ReloadCache")
	anzStringQuery(t, client, 6, "", "*prefix:~*hdr.RequestMethod:AdminSv1.")

	// Query results that happen before a given time
	contentFltr := "*lt:~*hdr.RequestStartTime:%s"
	contentFltr = fmt.Sprintf(contentFltr, timeVar)
	anzStringQuery(t, client, 1, "", contentFltr)

	anzStringQuery(t, client, 7, "", "*lte:~*hdr.RequestID:2")
	anzStringQuery(t, client, -1, "", "*gt:~*hdr.RequestDuration:500µs")
	anzStringQuery(t, client, 1, `+RequestMethod:"AttributeSv1.ProcessEvent"`, "*notstring:~*rep.CGREvent.Event.Cost:0")
	anzStringQuery(t, client, 1, `+RequestMethod:"CoreSv1.Status"`, "*gt:~*rep.goroutines:47")
}

// anzStringQuery sends an AnalyzerSv1.StringQuery request. First filter represents
// the HeaderFilters parameter, while the rest are the ContentFilters. Checks if the
// result contains the expected amount of matches (wantRC).
func anzStringQuery(t *testing.T, client *birpc.Client, wantRC int, filters ...string) {
	t.Helper()
	var headerFilters string
	var contentFilters []string
	if len(filters) > 0 {
		headerFilters = filters[0]
		contentFilters = filters[1:]
	}
	var result []map[string]any
	if err := client.Call(context.Background(), utils.AnalyzerSv1StringQuery,
		&analyzers.QueryArgs{
			HeaderFilters:  headerFilters,
			ContentFilters: contentFilters,
		}, &result); err != nil {
		t.Error(err)
	} else if len(result) != wantRC && wantRC != -1 {
		t.Errorf("AnalyzerSv1.StringQuery: len(result)=%d, want %d\n%s", len(result), wantRC, utils.ToJSON(result))
	} else if wantRC == -1 && len(result) < 1 {
		t.Errorf("AnalyzerSv1.StringQuery: len(result)=%d, want >0\n%s", len(result), utils.ToJSON(result))
	}

}

func setAttrProfiles(t *testing.T, client *birpc.Client) {
	attributeProfiles := []*utils.APIAttributeProfileWithAPIOpts{
		{
			APIAttributeProfile: &utils.APIAttributeProfile{
				ID:        "ATTR_1001",
				Tenant:    "cgrates.org",
				FilterIDs: []string{"*string:~*req.Account:1001"},
				Weights: utils.DynamicWeights{
					{
						Weight: 30,
					},
				},
				Attributes: []*utils.ExternalAttribute{
					{
						FilterIDs: []string{"*notexists:~*req.RequestType:"},
						Path:      "*req.RequestType",
						Type:      utils.MetaConstant,
						Value:     "*rated",
					},
				},
			},
		},
		{
			APIAttributeProfile: &utils.APIAttributeProfile{
				ID:        "ATTR_1002",
				Tenant:    "cgrates.org",
				FilterIDs: []string{"*string:~*req.Account:1002"},
				Weights: utils.DynamicWeights{
					{
						Weight: 10,
					},
				},
				Attributes: []*utils.ExternalAttribute{
					{
						FilterIDs: []string{"*notexists:~*req.RequestType:"},
						Path:      "*req.RequestType",
						Type:      utils.MetaConstant,
						Value:     "*postpaid",
					},
				},
			},
		},
		{
			APIAttributeProfile: &utils.APIAttributeProfile{
				ID:        "ATTR_1101",
				Tenant:    "cgrates.org",
				FilterIDs: []string{"*string:~*req.Account:1101"},
				Weights: utils.DynamicWeights{
					{
						Weight: 20,
					},
				},
				Attributes: []*utils.ExternalAttribute{
					{
						FilterIDs: []string{"*notexists:~*req.RequestType:"},
						Path:      "*req.RequestType",
						Type:      utils.MetaConstant,
						Value:     "*prepaid",
					},
				},
			},
		},
		{
			APIAttributeProfile: &utils.APIAttributeProfile{
				ID:        "ATTR_1102",
				Tenant:    "cgrates.org",
				FilterIDs: []string{"*string:~*req.Account:1102"},
				Weights: utils.DynamicWeights{
					{
						Weight: 5,
					},
				},
				Attributes: []*utils.ExternalAttribute{
					{
						FilterIDs: []string{"*notexists:~*req.RequestType:"},
						Path:      "*req.RequestType",
						Type:      utils.MetaConstant,
						Value:     "*pseudoprepaid",
					},
				},
			},
		},
		{
			APIAttributeProfile: &utils.APIAttributeProfile{
				ID:        "ATTR_PRF_11",
				Tenant:    "cgrates.org",
				FilterIDs: []string{"*prefix:~*req.Account:11"},
				Weights: utils.DynamicWeights{
					{
						Weight: 25,
					},
				},
				Attributes: []*utils.ExternalAttribute{
					{
						FilterIDs: []string{"*prefix:~*req.Destination:10"},
						Path:      "*req.Cost",
						Type:      utils.MetaConstant,
						Value:     "0",
					},
				},
			},
		},
	}
	var reply string
	for _, attributeProfile := range attributeProfiles {
		if err := client.Call(context.Background(), utils.AdminSv1SetAttributeProfile,
			attributeProfile, &reply); err != nil {
			t.Error(err)
		}
	}
}

func getAttrProfiles(t *testing.T, client *birpc.Client) {
	expectedAttributeProfiles := []*utils.APIAttributeProfile{
		{
			ID:        "ATTR_1001",
			Tenant:    "cgrates.org",
			FilterIDs: []string{"*string:~*req.Account:1001"},
			Weights: utils.DynamicWeights{
				{
					Weight: 30,
				},
			},
			Attributes: []*utils.ExternalAttribute{
				{
					FilterIDs: []string{"*notexists:~*req.RequestType:"},
					Path:      "*req.RequestType",
					Type:      utils.MetaConstant,
					Value:     "*rated",
				},
			},
		},
		{
			ID:        "ATTR_1002",
			Tenant:    "cgrates.org",
			FilterIDs: []string{"*string:~*req.Account:1002"},
			Weights: utils.DynamicWeights{
				{
					Weight: 10,
				},
			},
			Attributes: []*utils.ExternalAttribute{
				{
					FilterIDs: []string{"*notexists:~*req.RequestType:"},
					Path:      "*req.RequestType",
					Type:      utils.MetaConstant,
					Value:     "*postpaid",
				},
			},
		},
		{
			ID:        "ATTR_1101",
			Tenant:    "cgrates.org",
			FilterIDs: []string{"*string:~*req.Account:1101"},
			Weights: utils.DynamicWeights{
				{
					Weight: 20,
				},
			},
			Attributes: []*utils.ExternalAttribute{
				{
					FilterIDs: []string{"*notexists:~*req.RequestType:"},
					Path:      "*req.RequestType",
					Type:      utils.MetaConstant,
					Value:     "*prepaid",
				},
			},
		},
		{
			ID:        "ATTR_1102",
			Tenant:    "cgrates.org",
			FilterIDs: []string{"*string:~*req.Account:1102"},
			Weights: utils.DynamicWeights{
				{
					Weight: 5,
				},
			},
			Attributes: []*utils.ExternalAttribute{
				{
					FilterIDs: []string{"*notexists:~*req.RequestType:"},
					Path:      "*req.RequestType",
					Type:      utils.MetaConstant,
					Value:     "*pseudoprepaid",
				},
			},
		},
		{
			ID:        "ATTR_PRF_11",
			Tenant:    "cgrates.org",
			FilterIDs: []string{"*prefix:~*req.Account:11"},
			Weights: utils.DynamicWeights{
				{
					Weight: 25,
				},
			},
			Attributes: []*utils.ExternalAttribute{
				{
					FilterIDs: []string{"*prefix:~*req.Destination:10"},
					Path:      "*req.Cost",
					Type:      utils.MetaConstant,
					Value:     "0",
				},
			},
		},
	}
	var replyAttributeProfiles []*utils.APIAttributeProfile
	if err := client.Call(context.Background(), utils.AdminSv1GetAttributeProfiles,
		&utils.ArgsItemIDs{
			Tenant: "cgrates.org",
		}, &replyAttributeProfiles); err != nil {
		t.Error(err)
	} else {
		sort.Slice(replyAttributeProfiles, func(i, j int) bool {
			return replyAttributeProfiles[i].ID < replyAttributeProfiles[j].ID
		})
		if !reflect.DeepEqual(replyAttributeProfiles, expectedAttributeProfiles) {
			t.Errorf("expected: <%+v>, \nreceived: <%+v>",
				utils.ToJSON(expectedAttributeProfiles), utils.ToJSON(replyAttributeProfiles))
		}
	}
}

func attrProcessEvents(t *testing.T, client *birpc.Client) {
	eventCall1001to1002 := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "call1001to1002",
		Event: map[string]any{
			utils.ToR:          utils.MetaVoice,
			utils.OriginID:     "abcdef",
			utils.OriginHost:   "192.168.1.1",
			utils.Tenant:       "cgrates.org",
			utils.Category:     "call",
			utils.AccountField: "1001",
			utils.Subject:      "1001",
			utils.Destination:  "1002",
			utils.SetupTime:    time.Unix(1383813745, 0).UTC(),
			utils.AnswerTime:   time.Unix(1383813748, 0).UTC(),
			utils.Usage:        10 * time.Second,
			utils.RunID:        utils.MetaDefault,
			utils.Cost:         1.01,
		},
	}

	eventCall1101to1002 := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "call1011to1002",
		Event: map[string]any{
			utils.ToR:          utils.MetaVoice,
			utils.OriginID:     "ghijkl",
			utils.OriginHost:   "192.168.1.1",
			utils.Tenant:       "cgrates.org",
			utils.Category:     "call",
			utils.AccountField: "1101",
			utils.Subject:      "1101",
			utils.Destination:  "1002",
			utils.SetupTime:    time.Unix(1383813748, 0).UTC(),
			utils.AnswerTime:   time.Unix(1383813755, 0).UTC(),
			utils.Usage:        15 * time.Second,
			utils.RunID:        utils.MetaDefault,
			utils.Cost:         1.51,
		},
	}
	eventCall1102to1001 := &utils.CGREvent{
		Tenant: "cgrates.org",
		ID:     "call1102to1001",
		Event: map[string]any{
			utils.ToR:          utils.MetaVoice,
			utils.OriginID:     "uvwxyz",
			utils.OriginHost:   "192.168.1.1",
			utils.Tenant:       "cgrates.org",
			utils.Category:     "call",
			utils.AccountField: "1102",
			utils.Subject:      "1102",
			utils.Destination:  "1001",
			utils.SetupTime:    time.Unix(1383813755, 0).UTC(),
			utils.AnswerTime:   time.Unix(1383813762, 0).UTC(),
			utils.Usage:        20 * time.Second,
			utils.RunID:        utils.MetaDefault,
			utils.Cost:         2.01,
		},
	}
	expectedReply := `{"AlteredFields":[{"MatchedProfileID":"cgrates.org:ATTR_1001","Fields":["*req.RequestType"]}],"CGREvent":{"Tenant":"cgrates.org","ID":"call1001to1002","Event":{"Account":"1001","AnswerTime":"2013-11-07T08:42:28Z","Category":"call","Cost":1.01,"Destination":"1002","OriginHost":"192.168.1.1","OriginID":"abcdef","RequestType":"*rated","RunID":"*default","SetupTime":"2013-11-07T08:42:25Z","Subject":"1001","Tenant":"cgrates.org","ToR":"*voice","Usage":10000000000},"APIOpts":{}}}`
	var rplyEv attributes.AttrSProcessEventReply
	if err := client.Call(context.Background(), utils.AttributeSv1ProcessEvent,
		eventCall1001to1002, &rplyEv); err != nil {
		t.Error(err)
	} else if jsonReply := utils.ToJSON(rplyEv); jsonReply != expectedReply {
		t.Errorf("expected: <%+v>, \nreceived: <%+v>",
			expectedReply, jsonReply)
	}
	expectedReply = `{"AlteredFields":[{"MatchedProfileID":"cgrates.org:ATTR_PRF_11","Fields":["*req.Cost"]},{"MatchedProfileID":"cgrates.org:ATTR_1101","Fields":["*req.RequestType"]}],"CGREvent":{"Tenant":"cgrates.org","ID":"call1011to1002","Event":{"Account":"1101","AnswerTime":"2013-11-07T08:42:35Z","Category":"call","Cost":"0","Destination":"1002","OriginHost":"192.168.1.1","OriginID":"ghijkl","RequestType":"*prepaid","RunID":"*default","SetupTime":"2013-11-07T08:42:28Z","Subject":"1101","Tenant":"cgrates.org","ToR":"*voice","Usage":15000000000},"APIOpts":{}}}`
	if err := client.Call(context.Background(), utils.AttributeSv1ProcessEvent,
		eventCall1101to1002, &rplyEv); err != nil {
		t.Error(err)
	} else if jsonReply := utils.ToJSON(rplyEv); jsonReply != expectedReply {
		t.Errorf("expected: <%+v>, \nreceived: <%+v>",
			expectedReply, jsonReply)
	}
	expectedReply = `{"AlteredFields":[{"MatchedProfileID":"cgrates.org:ATTR_PRF_11","Fields":["*req.Cost"]},{"MatchedProfileID":"cgrates.org:ATTR_1102","Fields":["*req.RequestType"]}],"CGREvent":{"Tenant":"cgrates.org","ID":"call1102to1001","Event":{"Account":"1102","AnswerTime":"2013-11-07T08:42:42Z","Category":"call","Cost":"0","Destination":"1001","OriginHost":"192.168.1.1","OriginID":"uvwxyz","RequestType":"*pseudoprepaid","RunID":"*default","SetupTime":"2013-11-07T08:42:35Z","Subject":"1102","Tenant":"cgrates.org","ToR":"*voice","Usage":20000000000},"APIOpts":{}}}`
	if err := client.Call(context.Background(), utils.AttributeSv1ProcessEvent,
		eventCall1102to1001, &rplyEv); err != nil {
		t.Error(err)
	} else if jsonReply := utils.ToJSON(rplyEv); jsonReply != expectedReply {
		t.Errorf("expected: <%+v>, \nreceived: <%+v>",
			expectedReply, jsonReply)
	}
}
