package service

import (
	"testing"

	"github.com/mhsanaei/3x-ui/v3/database"
	"github.com/mhsanaei/3x-ui/v3/database/model"
	"github.com/mhsanaei/3x-ui/v3/web/runtime"
)

// While a node is config-dirty (a local edit committed before it could be
// mirrored to the node), the traffic pull must not overwrite the central
// inbound's config columns from the node's stale snapshot — only traffic
// counters may advance. Otherwise a reconnecting node reverts the edit.
func TestSetRemoteTraffic_DirtyPreservesConfig(t *testing.T) {
	setupConflictDB(t)
	db := database.GetDB()

	node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
	if err := db.Create(node).Error; err != nil {
		t.Fatalf("create node: %v", err)
	}
	id := node.Id

	const desiredSettings = `{"clients":[{"email":"a@x"}]}`
	central := &model.Inbound{
		UserId:   1,
		NodeID:   &id,
		Tag:      "in-443-tcp",
		Enable:   true,
		Port:     443,
		Protocol: model.VLESS,
		Settings: desiredSettings,
	}
	if err := db.Create(central).Error; err != nil {
		t.Fatalf("create inbound: %v", err)
	}

	snap := &runtime.TrafficSnapshot{
		Inbounds: []*model.Inbound{{
			Tag:      "in-443-tcp",
			Enable:   true,
			Port:     443,
			Protocol: model.VLESS,
			Settings: `{"clients":[{"email":"b@x"}]}`,
			Up:       500,
			Down:     700,
		}},
	}

	svc := InboundService{}
	if _, err := svc.setRemoteTrafficLocked(id, snap, true); err != nil {
		t.Fatalf("setRemoteTrafficLocked dirty: %v", err)
	}

	var got model.Inbound
	if err := db.First(&got, central.Id).Error; err != nil {
		t.Fatalf("reload inbound: %v", err)
	}
	if got.Settings != desiredSettings {
		t.Fatalf("dirty pull overwrote settings: want %q got %q", desiredSettings, got.Settings)
	}
	if got.Up != 500 || got.Down != 700 {
		t.Fatalf("traffic counters not applied while dirty: up=%d down=%d", got.Up, got.Down)
	}
}

// ClearNodeDirty must be a compare-and-swap on config_dirty_at so a concurrent
// edit that re-dirties the node during a reconcile is not silently cleared.
func TestNodeDirty_ClearIsCASOnDirtyAt(t *testing.T) {
	setupConflictDB(t)
	db := database.GetDB()

	node := &model.Node{Name: "n2", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
	if err := db.Create(node).Error; err != nil {
		t.Fatalf("create node: %v", err)
	}

	nodeSvc := NodeService{}
	if err := nodeSvc.MarkNodeDirty(node.Id); err != nil {
		t.Fatalf("MarkNodeDirty: %v", err)
	}
	_, _, dirty, dirtyAt, err := nodeSvc.NodeSyncState(node.Id)
	if err != nil {
		t.Fatalf("NodeSyncState: %v", err)
	}
	if !dirty {
		t.Fatal("node should be dirty after MarkNodeDirty")
	}

	if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt-1); err != nil {
		t.Fatalf("ClearNodeDirty stale token: %v", err)
	}
	if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); !stillDirty {
		t.Fatal("stale-token clear must not clear the dirty flag")
	}

	if err := nodeSvc.ClearNodeDirty(node.Id, dirtyAt); err != nil {
		t.Fatalf("ClearNodeDirty matching token: %v", err)
	}
	if _, _, stillDirty, _, _ := nodeSvc.NodeSyncState(node.Id); stillDirty {
		t.Fatal("matching-token clear must clear the dirty flag")
	}
}
