//go: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 sessions

import (
	"fmt"
	"testing"
	"time"

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

func TestSessionBasics(t *testing.T) {
	var dbcfg engine.DBCfg
	switch *utils.DBType {
	case utils.MetaInternal:
		dbcfg = engine.InternalDBCfg
	case utils.MetaRedis:
		dbcfg = engine.DBCfg{
			DB: &engine.DBParams{
				DBConns: map[string]engine.DBConn{
					utils.MetaDefault: {
						Type: utils.StringPointer(utils.MetaRedis),
						Host: utils.StringPointer("127.0.0.1"),
						Port: utils.IntPointer(6379),
						Name: utils.StringPointer("10"),
						User: utils.StringPointer(utils.CGRateSLwr),
					},
				},
			},
		}
	case utils.MetaMySQL:
		dbcfg = engine.MySQLDBCfg
	case utils.MetaMongo:
		dbcfg = engine.MongoDBCfg
	case utils.MetaPostgres:
		dbcfg = engine.PostgresDBCfg
	default:
		t.Fatal("unsupported dbtype value")
	}

	ng := engine.TestEngine{
		ConfigJSON: `{
"logger": {
	"type": "*stdout"
},
"db": {
	"db_conns": {
		"*default": {
			"db_type": "*internal",
			"opts":{
			"internalDBRewriteInterval": "0s",
			"internalDBDumpInterval": "0s"
		}
    	}
	},
},
"sessions": {
    "enabled": true,
    "accounts_conns": ["*internal"],
	"rates_conns": ["*internal"],
    "cdrs_conns": ["*internal"]
},
"cdrs": {
    "enabled": true,
    "accounts_conns": ["*internal"],
	"rates_conns": ["*internal"]
},
"accounts": {
    "enabled": true,
	"rates_conns": ["*internal"]
},
"admins": {
    "enabled": true
},
"rates": {
	"enabled": true
}
}`,
		TpFiles: map[string]string{
			utils.RatesCsv: `
#Tenant,ID,FilterIDs,Weights,MinCost,MaxCost,MaxCostStrategy,RateID,RateFilterIDs,RateActivationStart,RateWeights,RateBlocker,RateIntervalStart,RateFixedFee,RateRecurrentFee,RateUnit,RateIncrement
cgrates.org,RP_STANDARD,,;10,,,,RT_STANDARD,*string:~*req.Destination:1002,"* * * * *",;10,false,0s,1,1,1m,1m
cgrates.org,RP_STANDARD,,,,,,RT_STANDARD,,,,,1m,0,0.6,1m,1s
cgrates.org,RP_FALLBACK,,;0,,,,RT_FALLBACK,*string:~*req.Destination:1002,"* * * * *",;0,false,0s,0,0.01,1s,1s`,
		},
		DBCfg:    dbcfg,
		Encoding: *utils.Encoding,
		// LogBuffer: new(bytes.Buffer),
	}
	// t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
	client, _ := ng.Run(t)

	// account helpers
	setAccount := func(t *testing.T, id string, balances []*utils.Balance) {
		t.Helper()
		acnt := &utils.AccountWithAPIOpts{
			Account: &utils.Account{
				Tenant: "cgrates.org",
				ID:     id,
				FilterIDs: []string{
					fmt.Sprintf("*string:~*req.Account:%s", id),
				},
			},
		}
		acnt.Balances = make(map[string]*utils.Balance)
		for _, bal := range balances {
			acnt.Balances[bal.ID] = bal
		}

		var replySet string
		if err := client.Call(context.Background(), utils.AdminSv1SetAccount,
			acnt, &replySet); err != nil {
			t.Error(err)
		}
	}

	checkAccountBalances := func(t *testing.T, acntID string, wantBalances map[string]float64) {
		t.Helper()
		var acnt utils.Account
		if err := client.Call(context.Background(), utils.AdminSv1GetAccount,
			&utils.TenantIDWithAPIOpts{
				TenantID: &utils.TenantID{
					Tenant: "cgrates.org",
					ID:     "1001",
				},
			}, &acnt); err != nil {
			t.Fatal(err)
		}
		for blncID, val := range wantBalances {
			gotUnits := acnt.Balances[blncID].Units
			wantUnits := utils.NewDecimalFromFloat64(val)
			if gotUnits.Compare(wantUnits) != 0 {
				t.Errorf("acnt %q balance %q units=%s, want %s",
					acntID, blncID, gotUnits.String(), wantUnits.String())
			}
		}
	}

	// cdr helpers
	cdrNo := 0
	processCDR := func(t *testing.T, acnt, dest, usage string, flags ...string) *utils.CDR {
		t.Helper()
		cdrNo++
		originID := fmt.Sprintf("processCDR%d", cdrNo)
		cgrEv := &utils.CGREvent{
			Tenant: "cgrates.org",
			Event: map[string]any{
				utils.AccountField: acnt,
				utils.Destination:  dest,
				utils.AnswerTime:   "2018-01-07T17:00:00Z",
			},
			APIOpts: map[string]any{
				utils.MetaOriginID: originID,
				utils.MetaUsage:    usage,
			},
		}
		for _, flag := range flags {
			cgrEv.APIOpts[flag] = true
		}
		var rplyProcCDR string
		if err := client.Call(context.Background(), utils.SessionSv1ProcessCDR,
			cgrEv, &rplyProcCDR); err != nil {
			t.Error(err)
		}

		var cdrs []*utils.CDR
		if err := client.Call(context.Background(), utils.AdminSv1GetCDRs,
			&utils.CDRFilters{
				FilterIDs: []string{
					fmt.Sprintf("*string:~*opts.*originID:%s", originID),
					"*exists:~*opts.*originID:",
					"*notexists:~*req.NonExistentField:",
					"*notempty:~*opts.*originID:",
				},
			}, &cdrs); err != nil {
			t.Fatal(err)
		}
		if len(cdrs) != 1 {
			t.Fatalf("%s received %d cdrs, want exactly one", utils.AdminSv1GetCDRs, len(cdrs))
		}
		return cdrs[0]
	}

	getCostDetails := func(t *testing.T, cdr *utils.CDR, field string) map[string]any {
		t.Helper()
		v, has := cdr.Opts[field]
		if !has {
			t.Fatalf("missing %q field in CDR opts", field)
		}
		costDetails, ok := v.(map[string]any)
		if !ok {
			t.Fatalf("cdr field %q of wrong type %T, want map[string]any", field, v)
		}
		return costDetails
	}

	checkCDR := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]float64) {
		t.Helper()
		var got float64
		for costKey, want := range wantCosts {
			switch costKey {
			case utils.Abstracts, utils.Concretes:
				cd := getCostDetails(t, cdr, utils.MetaAccountsCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				var canCast bool
				got, canCast = cd[costKey].(float64)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
				}
			case utils.Cost:
				cd := getCostDetails(t, cdr, utils.MetaRateSCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				var canCast bool
				got, canCast = cd[costKey].(float64)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
				}
			case utils.MetaCost:
				var canCast bool
				got, canCast = cdr.Opts[utils.MetaCost].(float64)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
				}
			default:
				t.Fatalf("invalid cdr cost key: %q", costKey)
			}
			if got != want {
				t.Errorf("cdr %s = %g, want %g", costKey, got, want)
			}
		}
	}

	checkCDRMongo := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]float64) {
		t.Helper()
		var got float64
		for costKey, want := range wantCosts {
			switch costKey {
			case "abstracts", "concretes":
				cd := getCostDetails(t, cdr, utils.MetaAccountsCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				got = cd[costKey].(float64)
			case "cost":
				cd := getCostDetails(t, cdr, utils.MetaRateSCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				got = cd[costKey].(float64)
			case utils.MetaCost:
				got = cdr.Opts[utils.MetaCost].(float64)
			default:
				t.Fatalf("invalid cdr cost key: %q", costKey)
			}
			if got != want {
				t.Errorf("cdr %s = %g, want %g", costKey, got, want)
			}
		}
	}

	checkCDRRedis := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]string) {
		t.Helper()
		var got string
		for costKey, want := range wantCosts {
			switch costKey {
			case utils.Abstracts, utils.Concretes:
				cd := getCostDetails(t, cdr, utils.MetaAccountsCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				var canCast bool
				got, canCast = cd[costKey].(string)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
				}
			case utils.Cost:
				cd := getCostDetails(t, cdr, utils.MetaRateSCost)
				if cd == nil {
					t.Fatalf("Nil costDetails")
				}
				var canCast bool
				got, canCast = cd[costKey].(string)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
				}
			case utils.MetaCost:
				var canCast bool
				got, canCast = cdr.Opts[utils.MetaCost].(string)
				if !canCast {
					t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
				}
			default:
				t.Fatalf("invalid cdr cost key: %q", costKey)
			}
			if got != want {
				t.Errorf("cdr %s = %v, want %v", costKey, got, want)
			}
		}
	}

	// session helpers
	authEvent := func(t *testing.T, wantUsage, wantErr string) {
		t.Helper()
		var reply V1AuthorizeReply
		err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent,
			&utils.CGREvent{
				Tenant: "cgrates.org",
				APIOpts: map[string]any{
					utils.MetaAccounts: true,
				},
				Event: map[string]any{
					utils.AccountField: "1001",
					utils.Destination:  "1002",
					utils.SetupTime:    "2018-01-07T17:00:00Z",
				},
			}, &reply)
		assertError(t, utils.SessionSv1AuthorizeEvent, err, wantErr)
		if err == nil {
			wantDecimal := utils.NewDecimalFromUsageIgnoreErr(wantUsage)
			if reply.MaxUsage.Compare(wantDecimal) != 0 {
				t.Errorf("%s reply.MaxUsage=%s, want %s",
					utils.SessionSv1AuthorizeEvent, reply.MaxUsage, wantDecimal)
				t.Logf("%s reply: %s", utils.SessionSv1AuthorizeEvent, utils.ToJSON(reply))
			}
		}
	}

	authEventWithDigest := func(t *testing.T, wantUsage time.Duration, wantErr string) {
		t.Helper()
		var reply V1AuthorizeReplyWithDigest
		err := client.Call(context.Background(), utils.SessionSv1AuthorizeEventWithDigest,
			&utils.CGREvent{
				Tenant: "cgrates.org",
				APIOpts: map[string]any{
					utils.MetaAccounts: true,
				},
				Event: map[string]any{
					utils.AccountField: "1001",
					utils.Destination:  "1002",
					utils.SetupTime:    "2018-01-07T17:00:00Z",
				},
			}, &reply)
		assertError(t, utils.SessionSv1AuthorizeEventWithDigest, err, wantErr)
		if err == nil {
			if got := time.Duration(wantUsage).Nanoseconds(); got != reply.MaxUsage {
				t.Errorf("%s reply.MaxUsage=%d, want %d", utils.SessionSv1AuthorizeEventWithDigest, reply.MaxUsage, got)
				t.Logf("%s reply: %s", utils.SessionSv1AuthorizeEvent, utils.ToJSON(reply))
			}
		}
	}

	t.Run("auth and cdr", func(t *testing.T) {
		// Account requested not found, should fail here with error
		authEvent(t, "", "ACCOUNTS_ERROR:NOT_FOUND")

		// Available less than requested(1m)
		setAccount(t, "1001", []*utils.Balance{
			{
				ID:      "ABSTRACT1",
				Type:    utils.MetaAbstract,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 20.0}},
				CostIncrements: []*utils.CostIncrement{
					{
						Increment:    utils.NewDecimalFromUsageIgnoreErr("1s"),
						RecurrentFee: utils.NewDecimalFromFloat64(0.01),
					},
				},
				Units: utils.NewDecimalFromUsageIgnoreErr("1m"),
			},
			{
				ID:      "CONCRETE1",
				Type:    utils.MetaConcrete,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
				CostIncrements: []*utils.CostIncrement{
					{
						Increment:    utils.NewDecimalFromUsageIgnoreErr("1s"),
						RecurrentFee: utils.NewDecimalFromFloat64(0.01),
					},
				},
				Units: utils.NewDecimalFromFloat64(0.5),
			},
		})

		authEvent(t, "50s", "")
		authEventWithDigest(t, 50*time.Second, "")

		setAccount(t, "1001", []*utils.Balance{
			{
				ID:      "CONCRETE1",
				Type:    utils.MetaConcrete,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
				CostIncrements: []*utils.CostIncrement{
					{
						Increment:    utils.NewDecimalFromUsageIgnoreErr("1s"),
						RecurrentFee: utils.NewDecimalFromFloat64(0.01),
					},
				},
				Units: utils.NewDecimalFromFloat64(10),
			},
		})
		authEvent(t, "1m", "")
		authEventWithDigest(t, time.Minute, "")

		// accounting via CostIncrements
		cdr := processCDR(t, "1001", "1002", "1m30s", utils.MetaAccounts)
		switch *utils.DBType {
		case utils.MetaMongo: // field names are lowercase on mongo
			checkCDRMongo(t, cdr,
				map[string]float64{
					"abstracts":    90000000000.0,
					"concretes":    0.9,
					utils.MetaCost: 0.9,
				})
		case utils.MetaRedis:
			checkCDRRedis(t, cdr,
				map[string]string{
					utils.Abstracts: "90000000000",
					utils.Concretes: "0.9",
					utils.MetaCost:  "0.9",
				})
		default:
			checkCDR(t, cdr,
				map[string]float64{
					utils.Abstracts: 90000000000.0,
					utils.Concretes: 0.9,
					utils.MetaCost:  0.9,
				})
		}

		checkAccountBalances(t, "1001", map[string]float64{
			"CONCRETE1": 9.1,
		})
	})

	t.Run("rates accounting", func(t *testing.T) {
		setAccount(t, "1001", []*utils.Balance{
			{
				ID:      "CONCRETE1",
				Type:    utils.MetaConcrete,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
				Units:   utils.NewDecimalFromFloat64(10),
			},
		})
		cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaAccounts)
		switch *utils.DBType {
		case utils.MetaMongo: // field names are lowercase on mongo
			checkCDRMongo(t, cdr,
				map[string]float64{
					"abstracts":    float64(150 * time.Second),
					"concretes":    2.9,
					utils.MetaCost: 2.9,
				})
		case utils.MetaRedis:
			checkCDRRedis(t, cdr,
				map[string]string{
					utils.Abstracts: "150000000000",
					utils.Concretes: "2.90",
					utils.MetaCost:  "2.90",
				})
		default:
			checkCDR(t, cdr,
				map[string]float64{
					utils.Abstracts: float64(150 * time.Second),
					utils.Concretes: 2.9,
					utils.MetaCost:  2.9,
				})
		}
		checkAccountBalances(t, "1001", map[string]float64{
			"CONCRETE1": 7.1,
		})
	})

	t.Run("rating", func(t *testing.T) {
		setAccount(t, "1001", []*utils.Balance{
			{
				ID:      "CONCRETE1",
				Type:    utils.MetaConcrete,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
				Units:   utils.NewDecimalFromFloat64(10),
			},
		})
		cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaRates)
		switch *utils.DBType {
		case utils.MetaMongo: // field names are lowercase on mongo
			checkCDRMongo(t, cdr,
				map[string]float64{
					"cost":         2.9,
					utils.MetaCost: 2.9,
				})
		case utils.MetaRedis:
			checkCDRRedis(t, cdr,
				map[string]string{
					utils.Cost:     "2.90",
					utils.MetaCost: "2.90",
				})
		default:
			checkCDR(t, cdr,
				map[string]float64{
					utils.Cost:     2.9,
					utils.MetaCost: 2.9,
				})
		}
	})

	t.Run("rates accounting with fallback", func(t *testing.T) {
		t.Skip("looping through all max_increments inside maxDebitAbstractsFromConcretes")
		setAccount(t, "1001", []*utils.Balance{
			{
				ID:      "CONCRETE1",
				Type:    utils.MetaConcrete,
				Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
				Units:   utils.NewDecimalFromFloat64(2.9), // balance only enough for 2m30s usage
			},
		})
		cdr := processCDR(t, "1001", "1002", "3m15s", utils.MetaAccounts)
		if *utils.DBType == utils.MetaMongo { // field names are lowercase on mongo
			checkCDRMongo(t, cdr,
				map[string]float64{
					"abstracts":    float64(150 * time.Second),
					"concretes":    2.9,
					utils.MetaCost: 2.9,
				})
		} else {
			checkCDR(t, cdr,
				map[string]float64{
					utils.Abstracts: float64(150 * time.Second),
					utils.Concretes: 2.9,
					utils.MetaCost:  2.9,
				})
		}
		checkAccountBalances(t, "1001", map[string]float64{
			"CONCRETE1": 7.1,
		})
	})

	t.Run("rating and accounting", func(t *testing.T) {
		setAccount(t, "1001", []*utils.Balance{
			{
				ID:   "CONCRETE1",
				Type: utils.MetaConcrete,
				CostIncrements: []*utils.CostIncrement{
					{
						Increment:    utils.NewDecimalFromUsageIgnoreErr("1s"),
						RecurrentFee: utils.NewDecimalFromFloat64(0.01),
					},
				},
				Units: utils.NewDecimalFromFloat64(10),
			},
		})
		cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaAccounts, utils.MetaRates)
		switch *utils.DBType {
		case utils.MetaMongo: // field names are lowercase on mongo
			checkCDRMongo(t, cdr,
				map[string]float64{
					"abstracts":    float64(150 * time.Second),
					"concretes":    1.5,
					utils.MetaCost: 1.5,
					"cost":         2.9,
				})
		case utils.MetaRedis:
			checkCDRRedis(t, cdr,
				map[string]string{
					utils.Abstracts: "150000000000",
					utils.Concretes: "1.5",
					utils.MetaCost:  "1.5",
					utils.Cost:      "2.90",
				})
		default:
			checkCDR(t, cdr,
				map[string]float64{
					utils.Abstracts: float64(150 * time.Second),
					utils.Concretes: 1.5,
					utils.MetaCost:  1.5,
					utils.Cost:      2.9,
				})
		}
		checkAccountBalances(t, "1001", map[string]float64{
			"CONCRETE1": 8.5,
		})
	})
}

func assertError(t *testing.T, method string, err error, wantErr string) {
	t.Helper()
	if wantErr == "" {
		if err != nil {
			t.Fatalf("%s: unexpected error: got %v, want none", method, err)
		}
	} else {
		if err == nil {
			t.Fatalf("%s: expected error %q, got none", method, wantErr)
		}
		if err.Error() != wantErr {
			t.Fatalf("%s: error mismatch: got %q, want %q", method, err.Error(), wantErr)
		}
	}
}

func TestSessionLifecycle(t *testing.T) {
	var dbcfg engine.DBCfg
	switch *utils.DBType {
	case utils.MetaInternal:
		dbcfg = engine.InternalDBCfg
	case utils.MetaRedis:
		dbcfg = engine.RedisDBCfg
	case utils.MetaMySQL:
		dbcfg = engine.MySQLDBCfg
	case utils.MetaMongo:
		dbcfg = engine.MongoDBCfg
	case utils.MetaPostgres:
		dbcfg = engine.PostgresDBCfg
	default:
		t.Fatal("unsupported dbtype value")
	}

	ng := engine.TestEngine{
		ConfigJSON: `{
"db": {
	"db_conns": {
		"*default": {
			"db_type": "*internal",
			"opts":{
			"internalDBRewriteInterval": "0s",
			"internalDBDumpInterval": "0s"
		}
    	}
	},
},
"sessions": {
	"enabled": true,
	"chargers_conns": ["*localhost"],
	"alterable_fields": ["AlterableField"],
	"terminate_attempts": 1
},
"chargers": {
	"enabled": true
},
"admins": {
	"enabled": true
}
}`,
		DBCfg:    dbcfg,
		Encoding: *utils.Encoding,
		// LogBuffer: new(bytes.Buffer),
	}
	// t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
	client, _ := ng.Run(t)

	initSession := func(t *testing.T, originID string) {
		t.Helper()
		var reply V1InitSessionReply
		err := client.Call(context.Background(), utils.SessionSv1InitiateSession,
			&utils.CGREvent{
				Tenant: "cgrates.org",
				Event: map[string]any{
					utils.AccountField: "1001",
					utils.Destination:  "1002",
					utils.SetupTime:    "2018-01-07T17:00:00Z",
					"AlterableField":   "test_val",
				},
				APIOpts: map[string]any{
					utils.MetaOriginID: originID,
					utils.MetaInitiate: true,
					utils.MetaChargers: true,
				},
			}, &reply)
		if err != nil {
			t.Fatalf("failed to initiate session %s: %v", originID, err)
		}
	}

	updateSession := func(t *testing.T, originID string) {
		t.Helper()
		var reply V1UpdateSessionReply
		err := client.Call(context.Background(), utils.SessionSv1UpdateSession,
			&utils.CGREvent{
				Tenant: "cgrates.org",
				Event: map[string]any{
					utils.AccountField: "1001",
					utils.Destination:  "1002",
					utils.SetupTime:    "2018-01-07T17:00:00Z",
					"AlterableField":   "new_val",
				},
				APIOpts: map[string]any{
					utils.MetaOriginID: originID,
					utils.MetaUpdate:   true,
				},
			}, &reply)
		if err != nil {
			t.Fatalf("failed to update session %s: %v", originID, err)
		}
	}

	termSession := func(t *testing.T, originID string) {
		t.Helper()
		var reply string
		err := client.Call(context.Background(), utils.SessionSv1TerminateSession,
			&utils.CGREvent{
				Tenant: "cgrates.org",
				Event: map[string]any{
					utils.AccountField: "1001",
					utils.Destination:  "1002",
					utils.SetupTime:    "2018-01-07T17:00:00Z",
				},
				APIOpts: map[string]any{
					utils.MetaOriginID:  originID,
					utils.MetaTerminate: true,
				},
			}, &reply)
		if err != nil {
			t.Fatalf("failed to terminate session %s: %v", originID, err)
		}
	}

	checkActiveSessions := func(t *testing.T, wantCount int, filters ...string) []*ExternalSession {
		t.Helper()
		var sessions []*ExternalSession
		if err := client.Call(context.Background(), utils.SessionSv1GetActiveSessions,
			&utils.SessionFilter{
				Filters: filters,
			}, &sessions); err != nil {
			if wantCount == 0 && err.Error() == utils.ErrNotFound.Error() {
				t.Logf("no active sessions found (expected)")
				return nil
			}
			t.Fatalf("failed to get active sessions: %v", err)
		}
		if len(sessions) != wantCount {
			t.Fatalf("%s received %d sessions, want exactly %d",
				utils.SessionSv1GetActiveSessions, len(sessions), wantCount)
		}
		t.Logf("%s reply: %s", utils.SessionSv1GetActiveSessions, utils.ToIJSON(sessions))
		return sessions
	}

	var replySetCharger string
	if err := client.Call(context.Background(), utils.AdminSv1SetChargerProfile,
		&utils.ChargerProfileWithAPIOpts{
			ChargerProfile: &utils.ChargerProfile{
				Tenant:       "cgrates.org",
				ID:           "DEFAULT",
				RunID:        utils.MetaDefault,
				AttributeIDs: []string{utils.MetaNone},
			},
		}, &replySetCharger); err != nil {
		t.Fatal(err)
	}

	checkActiveSessions(t, 0)

	sessionID := "test-session-123"
	initSession(t, sessionID)

	sessions := checkActiveSessions(t, 1)
	want := "test_val"
	if sessions[0].CGREvent.Event["AlterableField"] != want {
		t.Errorf("after init, AlterableField = %v, want %s", sessions[0].CGREvent.Event["AlterableField"], want)
	}

	updateSession(t, sessionID)

	sessions = checkActiveSessions(t, 1)
	want = "new_val"
	if sessions[0].CGREvent.Event["AlterableField"] != want {
		t.Errorf("after update, AlterableField = %v, want %s", sessions[0].CGREvent.Event["AlterableField"], want)
	}

	termSession(t, sessionID)
	checkActiveSessions(t, 0)
}
