/*
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 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 General Public License for more details.

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

package engine

import (
	"reflect"
	"slices"
	"testing"
	"time"

	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/utils"
)

func TestIPProfileClone(t *testing.T) {
	activation := time.Date(2025, 7, 21, 10, 0, 0, 0, time.UTC)
	expiry := time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC)

	ip := &IPProfile{
		Tenant:    "cgrates.org",
		ID:        "ip_profile_1",
		FilterIDs: []string{"flt1", "flt2"},
		ActivationInterval: &utils.ActivationInterval{
			ActivationTime: activation,
			ExpiryTime:     expiry,
		},
		TTL:         10 * time.Minute,
		Type:        "ipv4",
		AddressPool: "main_pool",
		Allocation:  "dynamic",
		Stored:      true,
		Weight:      2.5,
	}

	cloned := ip.Clone()

	if cloned == ip {
		t.Error("Clone returned the same reference as original")
	}
	if cloned.Tenant != ip.Tenant {
		t.Error("Tenant mismatch")
	}
	if cloned.ID != ip.ID {
		t.Error("ID mismatch")
	}
	if cloned.Type != ip.Type {
		t.Error("Type mismatch")
	}
	if cloned.AddressPool != ip.AddressPool {
		t.Error("AddressPool mismatch")
	}
	if cloned.Allocation != ip.Allocation {
		t.Error("Allocation mismatch")
	}
	if cloned.Stored != ip.Stored {
		t.Error("Stored mismatch")
	}
	if cloned.Weight != ip.Weight {
		t.Error("Weight mismatch")
	}
	if cloned.TTL != ip.TTL {
		t.Error("TTL mismatch")
	}

	if cloned.ActivationInterval == ip.ActivationInterval {
		t.Error("ActivationInterval not deeply cloned")
	}
	if cloned.ActivationInterval.ActivationTime != ip.ActivationInterval.ActivationTime {
		t.Error("ActivationTime mismatch")
	}
	if cloned.ActivationInterval.ExpiryTime != ip.ActivationInterval.ExpiryTime {
		t.Error("ExpiryTime mismatch")
	}

	if len(cloned.FilterIDs) != len(ip.FilterIDs) {
		t.Error("FilterIDs length mismatch")
	}
	for i := range cloned.FilterIDs {
		if cloned.FilterIDs[i] != ip.FilterIDs[i] {
			t.Errorf("FilterIDs[%d] mismatch", i)
		}
	}

	cloned.FilterIDs[0] = "changed"
	if ip.FilterIDs[0] == "changed" {
		t.Error("Original FilterIDs changed after modifying clone")
	}

	var nilIP *IPProfile
	clonedNil := nilIP.Clone()
	if clonedNil != nil {
		t.Error("Clone of nil IPProfile should return nil")
	}
}

func TestIPProfileCacheClone(t *testing.T) {
	ip := &IPProfile{
		Tenant:    "cgrates.org",
		ID:        "ip_cache_test",
		FilterIDs: []string{"fltA"},
		ActivationInterval: &utils.ActivationInterval{
			ActivationTime: time.Date(2025, 7, 21, 0, 0, 0, 0, time.UTC),
			ExpiryTime:     time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
		},
		Type:        "ipv6",
		TTL:         5 * time.Minute,
		AddressPool: "test_pool",
		Allocation:  "static",
		Stored:      true,
		Weight:      1.0,
	}

	clonedAny := ip.CacheClone()
	cloned, ok := clonedAny.(*IPProfile)
	if !ok {
		t.Error("CacheClone did not return *IPProfile")
	}
	if cloned == ip {
		t.Error("CacheClone returned the same reference instead of a clone")
	}
	if cloned.ID != ip.ID || cloned.Tenant != ip.Tenant {
		t.Error("Cloned fields do not match original")
	}
	cloned.FilterIDs[0] = "modified"
	if ip.FilterIDs[0] == "modified" {
		t.Error("Original FilterIDs modified")
	}

	var nilIP *IPProfile
	clonedNil := nilIP.CacheClone()
	if clonedNil != nil {
		if _, ok := clonedNil.(*IPProfile); !ok {
			t.Error("CacheClone on nil receiver should return nil")
		}
	}
}

func TestIPProfileLockKey(t *testing.T) {
	tenant := "cgrates.org"
	id := "1001"

	got := ipProfileLockKey(tenant, id)
	want := utils.CacheIPProfiles + ":" + tenant + ":" + id
	if got != want {
		t.Errorf("ipProfileLockKey() = %q; want %q", got, want)
	}
	got = ipProfileLockKey("", "")
	want = utils.CacheIPProfiles + "::"
	if got != want {
		t.Errorf("ipProfileLockKey() with empty strings = %q; want %q", got, want)
	}
}

func TestIPProfilelock(t *testing.T) {
	ip := &IPProfile{
		Tenant: "cgrates.org",
		ID:     "profile123",
	}

	ip.lock("IDlock")
	if ip.lkID != "IDlock" {
		t.Errorf("expected lkID 'IDlock', got %q", ip.lkID)
	}

	ip.lock(utils.EmptyString)
	if ip.lkID == "" {
		t.Error("expected lkID to be set by guardian but got empty string")
	}
}

func TestIPProfileUnlock(t *testing.T) {
	ip := &IPProfile{}
	ip.lkID = utils.EmptyString
	ip.unlock()
	if ip.lkID != utils.EmptyString {
		t.Errorf("expected lkID to remain empty, got %q", ip.lkID)
	}
	ip.lkID = "Id"
	ip.unlock()
	if ip.lkID != utils.EmptyString {
		t.Errorf("expected lkID to be cleared, got %q", ip.lkID)
	}
}

func TestIPUsageTenantID(t *testing.T) {
	u := &IPUsage{
		Tenant: "cgrates.org",
		ID:     "usage01",
	}
	got := u.TenantID()
	want := "cgrates.org:usage01"
	if got != want {
		t.Errorf("TenantID() = %q; want %q", got, want)
	}

	u.Tenant = ""
	u.ID = ""
	got = u.TenantID()
	want = ":"
	if got != want {
		t.Errorf("TenantID() with empty = %q; want %q", got, want)
	}
}

func TestIPUsageIsActive(t *testing.T) {
	now := time.Now()

	u := &IPUsage{
		ExpiryTime: now.Add(1 * time.Hour),
	}
	if !u.isActive(now) {
		t.Errorf("Expected active usage, got inactive")
	}

	u.ExpiryTime = now.Add(-1 * time.Hour)
	if u.isActive(now) {
		t.Errorf("Expected inactive usage, got active")
	}

	u.ExpiryTime = time.Time{}
	if !u.isActive(now) {
		t.Errorf("Expected active usage for zero expiry time, got inactive")
	}
}

func TestIPTotalUsage(t *testing.T) {
	ip := &IP{
		Usages: map[string]*IPUsage{
			"u1": {Units: 1.5},
			"u2": {Units: 2.0},
			"u3": {Units: 0.5},
		},
	}

	expected := 4.0
	got := ip.TotalUsage()
	if got != expected {
		t.Errorf("TotalUsage() = %v, want %v", got, expected)
	}

	gotCached := ip.TotalUsage()
	if gotCached != expected {
		t.Errorf("TotalUsage() after cache = %v, want %v", gotCached, expected)
	}
}

func TestIPUsageClone(t *testing.T) {
	original := &IPUsage{
		Tenant:     "cgrates.org",
		ID:         "ID1001",
		ExpiryTime: time.Now().Add(24 * time.Hour),
	}

	cloned := original.Clone()

	if cloned == nil {
		t.Fatal("expected clone not to be nil")
	}
	if cloned == original {
		t.Error("expected clone to be a different instance")
	}
	if *cloned != *original {
		t.Errorf("expected clone to have same content, got %+v, want %+v", cloned, original)
	}

	var nilUsage *IPUsage
	nilClone := nilUsage.Clone()
	if nilClone != nil {
		t.Error("expected nil clone for nil input")
	}
}

func TestIPClone(t *testing.T) {
	ttl := 5 * time.Minute
	tUsage := 123.45
	dirty := true
	expTime := time.Now().Add(time.Hour)

	original := &IP{
		Tenant: "cgrates.org",
		ID:     "ip01",
		TTLIdx: []string{"idx1", "idx2"},
		cfg: &IPProfile{
			Tenant:    "cgrates.org",
			ID:        "profile1",
			FilterIDs: []string{"f1", "f2"},
			TTL:       time.Minute * 10,
			Type:      "dynamic",
		},
		Usages: map[string]*IPUsage{
			"u1": {
				Tenant:     "cgrates.org",
				ID:         "u1",
				ExpiryTime: expTime,
				Units:      50.0,
			},
		},
		ttl:    &ttl,
		tUsage: &tUsage,
		dirty:  &dirty,
	}

	cloned := original.Clone()

	t.Run("nil IP returns nil", func(t *testing.T) {
		var ip *IP = nil
		cloned := ip.Clone()
		if cloned != nil {
			t.Errorf("expected nil, got %+v", cloned)
		}
	})

	if !reflect.DeepEqual(original, cloned) {
		t.Errorf("Expected clone to be deeply equal to original, got difference:\noriginal: %+v\nclone: %+v", original, cloned)
	}

	if cloned == original {
		t.Error("Clone should return a different pointer than the original")
	}
	if cloned.cfg == original.cfg {
		t.Error("cfg field was not deeply cloned")
	}
	if cloned.Usages["u1"] == original.Usages["u1"] {
		t.Error("Usages map content was not deeply cloned")
	}
	if cloned.ttl == original.ttl {
		t.Error("ttl pointer was not deeply cloned")
	}
	if cloned.tUsage == original.tUsage {
		t.Error("tUsage pointer was not deeply cloned")
	}
	if cloned.dirty == original.dirty {
		t.Error("dirty pointer was not deeply cloned")
	}
}

func TestIpLockKey(t *testing.T) {
	tnt := "cgrates.org"
	id := "192.168.0.1"
	expected := utils.ConcatenatedKey(utils.CacheIPs, tnt, id)

	got := ipLockKey(tnt, id)
	if got != expected {
		t.Errorf("Expected %s, got %s", expected, got)
	}
}

func TestIPlock(t *testing.T) {
	ip := &IP{Tenant: "cgrates.org", ID: "1001"}

	ip.lock("customLockID")
	if ip.lkID != "customLockID" {
		t.Errorf("Expected lkID to be 'customLockID', got %s", ip.lkID)
	}

	ip2 := &IP{Tenant: "cgrates.org2", ID: "1002"}
	ip2.lock(utils.EmptyString)
	if ip2.lkID == utils.EmptyString {
		t.Error("Expected lkID to be set by Guardian.GuardIDs, got empty string")
	}
}

func TestIPUunlock(t *testing.T) {
	ip := &IP{lkID: "LockID"}

	ip.unlock()

	if ip.lkID != utils.EmptyString {
		t.Errorf("Expected lkID to be cleared, got %s", ip.lkID)
	}
	ip.unlock()
}

func TestIPRemoveExpiredUnits(t *testing.T) {
	now := time.Now()
	expiredID := "expired"
	activeID := "active"

	ip := &IP{
		ID:     "ip-test",
		Usages: map[string]*IPUsage{},
		TTLIdx: []string{expiredID, activeID},
		tUsage: utils.Float64Pointer(30.0),
	}

	ip.Usages[expiredID] = &IPUsage{
		ID:         expiredID,
		ExpiryTime: now.Add(-10 * time.Minute),
		Units:      10.0,
	}

	ip.Usages[activeID] = &IPUsage{
		ID:         activeID,
		ExpiryTime: now.Add(10 * time.Minute),
		Units:      20.0,
	}

	ip.removeExpiredUnits()

	if _, ok := ip.Usages[expiredID]; ok {
		t.Errorf("Expected expired usage to be removed")
	}
	if _, ok := ip.Usages[activeID]; !ok {
		t.Errorf("Expected active usage to be retained")
	}
	if ip.tUsage != nil {
		t.Errorf("Expected tUsage to be set to nil after recalculation")
	}
	if len(ip.TTLIdx) != 1 || ip.TTLIdx[0] != activeID {
		t.Errorf("Expected TTLIdx to only contain activeID")
	}
}

func TestIPRecordUsage(t *testing.T) {
	t.Run("record new usage with ttl set", func(t *testing.T) {
		ttl := 10 * time.Minute
		ip := &IP{
			Usages: make(map[string]*IPUsage),
			TTLIdx: []string{},
			ttl:    &ttl,
			tUsage: new(float64),
		}
		usage := &IPUsage{Tenant: "cgrates.org", ID: "usage1", Units: 5}
		err := ip.recordUsage(usage)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if got, want := len(ip.Usages), 1; got != want {
			t.Fatalf("unexpected number of usages: got %d, want %d", got, want)
		}
		if _, ok := ip.Usages[usage.ID]; !ok {
			t.Fatal("usage not recorded")
		}
		if len(ip.TTLIdx) != 1 || ip.TTLIdx[0] != usage.ID {
			t.Fatal("TTLIdx not updated properly")
		}
		if ip.tUsage == nil || *ip.tUsage != usage.Units {
			t.Fatalf("tUsage not updated properly, got %v", ip.tUsage)
		}
	})

	t.Run("duplicate usage id", func(t *testing.T) {
		ip := &IP{
			Usages: map[string]*IPUsage{
				"usage1": {Tenant: "cgrates.org", ID: "usage1", Units: 5},
			},
		}
		usage := &IPUsage{Tenant: "cgrates.org", ID: "usage1", Units: 10}
		err := ip.recordUsage(usage)
		if err == nil {
			t.Fatal("expected error on duplicate usage id, got nil")
		}
	})

	t.Run("ttl zero disables expiry setting", func(t *testing.T) {
		zeroTTL := time.Duration(0)
		ip := &IP{
			Usages: make(map[string]*IPUsage),
			ttl:    &zeroTTL,
		}
		usage := &IPUsage{Tenant: "cgrates.org", ID: "noexpiry", Units: 2}
		err := ip.recordUsage(usage)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(ip.Usages) != 0 {
			t.Fatal("usage should NOT be recorded when ttl is 0")
		}
	})
}

func TestIPClearUsage(t *testing.T) {
	t.Run("usage not found", func(t *testing.T) {
		ip := &IP{Usages: make(map[string]*IPUsage)}
		err := ip.clearUsage("missing")
		if err == nil {
			t.Fatal("expected error, got nil")
		}
		expected := "cannot find usage record with id: missing"
		if err.Error() != expected {
			t.Errorf("expected error %q, got %q", expected, err.Error())
		}
	})

	t.Run("usage found with zero expiry time", func(t *testing.T) {
		totalUsage := 10.0
		ip := &IP{
			Usages: map[string]*IPUsage{
				"id1": {Units: 5.0},
			},
			tUsage: &totalUsage,
			TTLIdx: []string{"id2"},
		}

		err := ip.clearUsage("id1")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		if _, exists := ip.Usages["id1"]; exists {
			t.Error("expected id1 to be deleted from Usages")
		}
		if *ip.tUsage != 5.0 {
			t.Errorf("expected tUsage=5.0, got %v", *ip.tUsage)
		}
		if !slices.Equal(ip.TTLIdx, []string{"id2"}) {
			t.Errorf("TTLIdx changed unexpectedly: %v", ip.TTLIdx)
		}
	})

	t.Run("usage found with non-zero expiry time", func(t *testing.T) {
		totalUsage := 20.0
		expTime := time.Now().Add(time.Hour)
		ip := &IP{
			Usages: map[string]*IPUsage{
				"id2": {ExpiryTime: expTime, Units: 7.0},
			},
			TTLIdx: []string{"id2", "other"},
			tUsage: &totalUsage,
		}

		err := ip.clearUsage("id2")
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}

		if _, exists := ip.Usages["id2"]; exists {
			t.Error("expected id2 to be deleted from Usages")
		}
		if *ip.tUsage != 13.0 {
			t.Errorf("expected tUsage=13.0, got %v", *ip.tUsage)
		}
		if slices.Contains(ip.TTLIdx, "id2") {
			t.Errorf("expected id2 to be removed from TTLIdx, got %v", ip.TTLIdx)
		}
	})
}

func TestIPsSort(t *testing.T) {
	ip1 := &IP{cfg: &IPProfile{Weight: 1.0}}
	ip2 := &IP{cfg: &IPProfile{Weight: 3.0}}
	ip3 := &IP{cfg: &IPProfile{Weight: 2.0}}

	ips := IPs{ip1, ip2, ip3}
	ips.Sort()

	expected := IPs{ip2, ip3, ip1}
	for i := range ips {
		if ips[i] != expected[i] {
			t.Errorf("expected index %d to be %+v, got %+v", i, expected[i], ips[i])
		}
	}
}

func TestIPsUnlock(t *testing.T) {
	ip1 := &IP{lkID: "lock1", cfg: &IPProfile{lkID: "cfgLock1"}}
	ip2 := &IP{lkID: "lock2", cfg: &IPProfile{lkID: "cfgLock2"}}
	ip3 := &IP{lkID: "lock3", cfg: nil}

	ips := IPs{ip1, ip2, ip3}
	ips.unlock()

	if ip1.lkID != "" {
		t.Errorf("expected ip1.lkID to be empty, got %q", ip1.lkID)
	}
	if ip2.lkID != "" {
		t.Errorf("expected ip2.lkID to be empty, got %q", ip2.lkID)
	}
	if ip3.lkID != "" {
		t.Errorf("expected ip3.lkID to be empty, got %q", ip3.lkID)
	}

	if ip1.cfg.lkID != "" {
		t.Errorf("expected ip1.cfg.lkID to be empty, got %q", ip1.cfg.lkID)
	}
	if ip2.cfg.lkID != "" {
		t.Errorf("expected ip2.cfg.lkID to be empty, got %q", ip2.cfg.lkID)
	}
}

func TestIPsIds(t *testing.T) {
	ip1 := &IP{ID: "ip1"}
	ip2 := &IP{ID: "ip2"}
	ip3 := &IP{ID: "ip3"}

	ips := IPs{ip1, ip2, ip3}

	got := ips.ids()

	if len(got) != 3 {
		t.Errorf("expected 3 IDs in set, got %d", len(got))
	}

	expectedIDs := []string{"ip1", "ip2", "ip3"}
	for _, id := range expectedIDs {
		if _, exists := got[id]; !exists {
			t.Errorf("expected ID %q in set, but it was missing", id)
		}
	}
}

func TestIPCacheClone(t *testing.T) {
	ttl := 10 * time.Minute
	tUsage := 123.45
	dirty := true

	original := &IP{
		Tenant: "cgrates.org",
		ID:     "ip01",
		TTLIdx: []string{"idx1", "idx2"},
		Usages: map[string]*IPUsage{
			"u1": {Tenant: "cgrates.org", ID: "u1", Units: 50.0},
		},
		ttl:    &ttl,
		tUsage: &tUsage,
		dirty:  &dirty,
		cfg: &IPProfile{
			Tenant: "cgrates.org",
			ID:     "profile1",
			Weight: 1.5,
		},
	}

	cloneAny := original.CacheClone()
	if cloneAny == nil {
		t.Fatal("CacheClone returned nil")
	}

	clone, ok := cloneAny.(*IP)
	if !ok {
		t.Fatalf("expected type *IP, got %T", cloneAny)
	}

	if clone == original {
		t.Error("expected clone to be a different pointer than original")
	}

	if !reflect.DeepEqual(clone, original) {
		t.Errorf("expected clone to be deeply equal to original:\noriginal: %+v\nclone: %+v", original, clone)
	}
}

func TestNewIPService(t *testing.T) {
	dm := &DataManager{}
	cgrcfg := &config.CGRConfig{}
	filterS := &FilterS{}
	connMgr := &ConnManager{}

	service := NewIPService(dm, cgrcfg, filterS, connMgr)
	if service == nil {
		t.Fatal("expected NewIPService to return non-nil")
	}

	if service.dm != dm {
		t.Errorf("expected dm=%+v, got %+v", dm, service.dm)
	}
	if service.cfg != cgrcfg {
		t.Errorf("expected cfg=%+v, got %+v", cgrcfg, service.cfg)
	}
	if service.fs != filterS {
		t.Errorf("expected fs=%+v, got %+v", filterS, service.fs)
	}
	if service.cm != connMgr {
		t.Errorf("expected cm=%+v, got %+v", connMgr, service.cm)
	}

	if service.storedIPs == nil {
		t.Error("expected storedIPs map to be initialized")
	}
	if service.loopStopped == nil {
		t.Error("expected loopStopped channel to be initialized")
	}
	if service.stopBackup == nil {
		t.Error("expected stopBackup channel to be initialized")
	}
}

func TestIPServiceStoreMatchedIPs(t *testing.T) {
	cfg := config.NewDefaultCGRConfig()
	data, err := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items)
	if err != nil {
		t.Error(err)
	}
	dm := NewDataManager(data, config.CgrConfig().CacheCfg(), nil)

	t.Run("StoreInterval=0", func(t *testing.T) {
		cfg.IPsCfg().StoreInterval = 0
		svc := &IPService{
			cfg:       cfg,
			dm:        dm,
			storedIPs: make(utils.StringSet),
		}

		dirty := false
		ip := &IP{Tenant: "cgrates.org", ID: "ip01", dirty: &dirty}

		err := svc.storeMatchedIPs(IPs{ip})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if dirty {
			t.Errorf("expected dirty=false, got true")
		}
		if len(svc.storedIPs) != 0 {
			t.Errorf("expected storedIPs empty, got %+v", svc.storedIPs)
		}
	})

	t.Run("StoreInterval>0 marks dirty and schedules for backup", func(t *testing.T) {
		cfg.IPsCfg().StoreInterval = 10
		svc := &IPService{
			cfg:       cfg,
			dm:        dm,
			storedIPs: make(utils.StringSet),
		}

		dirty := false
		ip := &IP{Tenant: "cgrates.org", ID: "ip02", dirty: &dirty}

		err := svc.storeMatchedIPs(IPs{ip})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if !dirty {
			t.Errorf("expected dirty=true, got false")
		}
		if _, exists := svc.storedIPs[ip.TenantID()]; !exists {
			t.Errorf("expected ip02 in storedIPs, got %+v", svc.storedIPs)
		}
	})

	t.Run("StoreInterval<0 stores immediately", func(t *testing.T) {
		cfg.IPsCfg().StoreInterval = -1
		svc := &IPService{
			cfg:       cfg,
			dm:        dm,
			storedIPs: make(utils.StringSet),
		}

		dirty := true
		ip := &IP{Tenant: "cgrates.org", ID: "ip03", dirty: &dirty}

		Cache.Set(utils.CacheIPs, ip.TenantID(), ip, nil, true, utils.NonTransactional)

		err := svc.storeMatchedIPs(IPs{ip})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if dirty {
			t.Errorf("expected dirty=false after storeIP, got true")
		}
	})
}

func TestIPServiceMatchingIPsForEvent(t *testing.T) {
	cfg := config.NewDefaultCGRConfig()
	data, err := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items)
	if err != nil {
		t.Error(err)
	}
	dm := NewDataManager(data, cfg.CacheCfg(), nil)
	fs := NewFilterS(cfg, nil, dm)

	svc := &IPService{
		cfg: cfg,
		dm:  dm,
		fs:  fs,
	}

	profile := &IPProfile{
		Tenant: "cgrates.org",
		ID:     "ip01",
		Stored: true,
		TTL:    5 * time.Minute,
	}
	if err := dm.SetIPProfile(profile, true); err != nil {
		t.Fatalf("failed to set IPProfile: %v", err)
	}

	ip := &IP{
		Tenant: "cgrates.org",
		ID:     "ip01",
	}
	Cache.Set(utils.CacheIPs, ip.TenantID(), ip, nil, true, utils.NonTransactional)
	if err := dm.SetIP(ip); err != nil {
		t.Fatalf("failed to set IP: %v", err)
	}

	ev := &utils.CGREvent{
		Event: map[string]interface{}{
			"type": "testEvent",
		},
		Time: func() *time.Time { t := time.Now(); return &t }(),
	}
	evUUID := "event-uuid-001"

	ips, err := svc.matchingIPsForEvent("cgrates.org", ev, evUUID, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(ips) != 1 {
		t.Errorf("expected 1 IP, got %d", len(ips))
	}
	if ips[0].ID != "ip01" {
		t.Errorf("expected IP ID 'ip01', got '%s'", ips[0].ID)
	}

	if ips[0].ttl == nil || *ips[0].ttl != profile.TTL {
		t.Errorf("expected IP TTL %v, got %v", profile.TTL, ips[0].ttl)
	}

	cached, ok := Cache.Get(utils.CacheEventIPs, evUUID)
	if !ok || cached == nil {
		t.Errorf("expected cached IPIDs for event UUID")
	}
}
