package scheduler

import (
	"context"
	"errors"
	"testing"

	"github.com/wangjia/pangolin/server/internal/provision"
	"github.com/wangjia/pangolin/server/internal/scheduler/orchestrate"
)

// fakeProvSvc records calls and returns canned results.
type fakeProvSvc struct {
	createSpec provision.NodeSpec
	createKey  string
	created    *provision.Node
	destroyID  int64
	rotateID   int64
	rotated    *provision.Node
	providers  []*provision.Provider
	lastPool   provision.Pool
	err        error
}

func (f *fakeProvSvc) CreateNode(_ context.Context, spec provision.NodeSpec, key string) (*provision.Node, error) {
	f.createSpec, f.createKey = spec, key
	return f.created, f.err
}
func (f *fakeProvSvc) DestroyNode(_ context.Context, id int64) error { f.destroyID = id; return f.err }
func (f *fakeProvSvc) RotateIP(_ context.Context, id int64) (*provision.Node, error) {
	f.rotateID = id
	return f.rotated, f.err
}
func (f *fakeProvSvc) ListProviders(_ context.Context, pool provision.Pool) ([]*provision.Provider, error) {
	f.lastPool = pool
	return f.providers, f.err
}

// fakeResolver maps known UUIDs → ids.
type fakeResolver struct{ byUUID map[string]*provision.Node }

func (f fakeResolver) GetNodeByUUID(_ context.Context, uuid string) (*provision.Node, error) {
	n, ok := f.byUUID[uuid]
	if !ok {
		return nil, errors.New("not found")
	}
	return n, nil
}

func TestProvisionAdapter_CreateNode_MapsSpecAndReturnsUUID(t *testing.T) {
	svc := &fakeProvSvc{created: &provision.Node{ID: 7, UUID: "uuid-7"}}
	a := NewProvisionAdapter(svc, fakeResolver{})

	id, err := a.CreateNode(context.Background(), orchestrate.NodeSpec{
		Tier: "pro", Region: "hkg", Role: "exit", ProviderID: "42",
		RealitySNI: "www.apple.com", RealityPBK: "pbk", HY2Port: 443,
		NameZH: "香港", NameEn: "HK",
	}, "idem-1")
	if err != nil {
		t.Fatalf("CreateNode: %v", err)
	}
	if id != "uuid-7" {
		t.Errorf("returned id = %q, want node UUID uuid-7", id)
	}
	if svc.createKey != "idem-1" {
		t.Errorf("idempotency key = %q, want idem-1", svc.createKey)
	}
	s := svc.createSpec
	if s.Region != "hkg" || s.Role != provision.Role("exit") || s.Tier != provision.TierPro ||
		s.ProviderID != 42 || s.RealitySNI != "www.apple.com" || s.RealityPBK != "pbk" ||
		s.HY2Port != 443 || s.NameZH != "香港" || s.NameEn != "HK" {
		t.Errorf("spec mapped wrong: %+v", s)
	}
}

func TestProvisionAdapter_DestroyAndRotate_ResolveUUID(t *testing.T) {
	svc := &fakeProvSvc{rotated: &provision.Node{ID: 9, UUID: "uuid-9"}}
	res := fakeResolver{byUUID: map[string]*provision.Node{"uuid-9": {ID: 9, UUID: "uuid-9"}}}
	a := NewProvisionAdapter(svc, res)

	if err := a.DestroyNode(context.Background(), "uuid-9"); err != nil {
		t.Fatalf("DestroyNode: %v", err)
	}
	if svc.destroyID != 9 {
		t.Errorf("destroy resolved id = %d, want 9", svc.destroyID)
	}

	newID, err := a.RotateIP(context.Background(), "uuid-9")
	if err != nil {
		t.Fatalf("RotateIP: %v", err)
	}
	if svc.rotateID != 9 || newID != "uuid-9" {
		t.Errorf("rotate id=%d newID=%q, want 9 / uuid-9", svc.rotateID, newID)
	}

	// Unknown UUID must error, not panic.
	if err := a.DestroyNode(context.Background(), "nope"); err == nil {
		t.Error("DestroyNode with unknown uuid should error")
	}
}

func TestProvisionAdapter_ListProviders_PoolAndRegionFilter(t *testing.T) {
	svc := &fakeProvSvc{providers: []*provision.Provider{
		{ID: 1, Regions: []string{"hkg", "tyo"}, Enabled: true},
		{ID: 2, Regions: []string{"sin"}, Enabled: true},      // wrong region
		{ID: 3, Regions: nil, Enabled: true},                  // all regions
		{ID: 4, Regions: []string{"hkg"}, Enabled: false},     // disabled
	}}
	a := NewProvisionAdapter(svc, fakeResolver{})

	got, err := a.ListProviders(context.Background(), "pro", "hkg")
	if err != nil {
		t.Fatalf("ListProviders: %v", err)
	}
	if svc.lastPool != provision.PoolPremium {
		t.Errorf("pool = %q, want premium (pro tier)", svc.lastPool)
	}
	// Expect providers 1 (region match) and 3 (all regions); 2 wrong region, 4 disabled.
	ids := map[string]bool{}
	for _, p := range got {
		ids[p.ID] = true
	}
	if !ids["1"] || !ids["3"] || ids["2"] || ids["4"] {
		t.Errorf("filtered providers = %v, want {1,3}", ids)
	}

	// free tier → consumable pool.
	_, _ = a.ListProviders(context.Background(), "free", "")
	if svc.lastPool != provision.PoolConsumable {
		t.Errorf("pool = %q, want consumable (free tier)", svc.lastPool)
	}
}
