package service

import (
	"context"
	"errors"
	"strings"
	"testing"

	edgenode "iop/apps/edge/internal/node"
	iop "iop/proto/gen/iop"
)

type fakeCredentialLeaseProvider struct {
	valid        bool
	acquireCalls int
	gotBinding   *iop.CredentialLeaseBinding
	mutateScope  func(*iop.CredentialLeaseScope)
}

func (p *fakeCredentialLeaseProvider) ValidateCredentialBinding(binding *iop.CredentialLeaseBinding) error {
	if !p.valid {
		return errors.New("stale")
	}
	return nil
}

func (p *fakeCredentialLeaseProvider) AcquireCredentialLease(_ context.Context, binding *iop.CredentialLeaseBinding, publicKey []byte) (*iop.SignedCredentialLease, error) {
	p.acquireCalls++
	p.gotBinding = binding
	if len(publicKey) != 32 {
		return nil, errors.New("bad recipient")
	}
	scope := &iop.CredentialLeaseScope{
		LeaseId: "lease-test", PrincipalRef: binding.GetPrincipalRef(), CredentialSlotRef: binding.GetCredentialSlotRef(),
		RouteId: binding.GetRouteId(), ProfileId: binding.GetProfileId(), UpstreamTarget: binding.GetUpstreamTarget(),
		NodeId: binding.GetNodeId(), RecipientKeyId: binding.GetRecipientKeyId(), HeaderName: "Authorization", Scheme: "Bearer",
		CredentialRevision: binding.GetCredentialRevision(), RouteRevision: binding.GetRouteRevision(),
		ProjectionGeneration: binding.GetProjectionGeneration(),
	}
	if p.mutateScope != nil {
		p.mutateScope(scope)
	}
	return &iop.SignedCredentialLease{Scope: scope}, nil
}

func TestAttachCredentialLeaseBindsSelectedCandidateAndUsesDedicatedFields(t *testing.T) {
	provider := &fakeCredentialLeaseProvider{valid: true}
	service := New(nil, nil)
	service.SetCredentialLeaseProvider(provider)
	service.SetCredentialLeaseLimit(1)
	req := SubmitProviderTunnelRequest{CredentialBinding: &CredentialBinding{
		PrincipalRef: "principal", CredentialSlotRef: "slot", RouteID: "route", ProfileID: "openai",
		CredentialRevision: 7, RouteRevision: 8, ProjectionGeneration: 9,
	}}
	entry := &edgenode.NodeEntry{NodeID: "selected-node", CredentialRecipientKeyID: "recipient-key", CredentialRecipientPublicKey: make([]byte, 32)}
	tunnel := &iop.ProviderTunnelRequest{Headers: map[string]string{"Content-Type": "application/json"}}
	if err := service.attachCredentialLease(context.Background(), req, entry, "selected-target", tunnel); err != nil {
		t.Fatal(err)
	}
	defer service.releaseCredentialLease()
	if provider.acquireCalls != 1 || provider.gotBinding.GetNodeId() != "selected-node" || provider.gotBinding.GetUpstreamTarget() != "selected-target" || provider.gotBinding.GetRecipientKeyId() != "recipient-key" {
		t.Fatalf("lease was not bound to selected candidate: %+v", provider.gotBinding)
	}
	if tunnel.GetCredentialLease() == nil || tunnel.GetCredentialBinding() == nil {
		t.Fatal("dedicated credential fields were not attached")
	}
	for key, value := range tunnel.GetHeaders() {
		if strings.Contains(strings.ToLower(key+value), "secret") || strings.EqualFold(key, "Authorization") {
			t.Fatalf("generic header contains provider credential material: %q", key)
		}
	}
}

func TestAttachCredentialLeaseFailsBeforeAcquireOnStaleBindingOrCollision(t *testing.T) {
	provider := &fakeCredentialLeaseProvider{valid: false}
	service := New(nil, nil)
	service.SetCredentialLeaseProvider(provider)
	service.SetCredentialLeaseLimit(1)
	req := SubmitProviderTunnelRequest{CredentialBinding: &CredentialBinding{PrincipalRef: "p", CredentialSlotRef: "s", RouteID: "r", ProfileID: "openai", ProjectionGeneration: 1}}
	entry := &edgenode.NodeEntry{NodeID: "node", CredentialRecipientKeyID: "key", CredentialRecipientPublicKey: make([]byte, 32)}
	if err := service.attachCredentialLease(context.Background(), req, entry, "target", &iop.ProviderTunnelRequest{}); err == nil || provider.acquireCalls != 0 {
		t.Fatalf("stale binding err=%v acquire_calls=%d", err, provider.acquireCalls)
	}
	provider.valid = true
	if err := service.attachCredentialLease(context.Background(), req, entry, "target", &iop.ProviderTunnelRequest{Headers: map[string]string{"authorization": "caller-value"}}); err == nil || !strings.Contains(err.Error(), "collision") {
		t.Fatalf("collision err=%v", err)
	}
	if provider.acquireCalls != 1 {
		t.Fatalf("collision acquisition calls=%d, want 1 signed-header check", provider.acquireCalls)
	}
}

func TestAttachCredentialLeaseRejectsMismatchedReturnedScope(t *testing.T) {
	provider := &fakeCredentialLeaseProvider{valid: true, mutateScope: func(scope *iop.CredentialLeaseScope) {
		scope.CredentialSlotRef = "different-slot"
	}}
	service := New(nil, nil)
	service.SetCredentialLeaseProvider(provider)
	service.SetCredentialLeaseLimit(1)
	req := SubmitProviderTunnelRequest{CredentialBinding: &CredentialBinding{
		PrincipalRef: "principal", CredentialSlotRef: "slot", RouteID: "route", ProfileID: "openai",
		CredentialRevision: 7, RouteRevision: 8, ProjectionGeneration: 9,
	}}
	entry := &edgenode.NodeEntry{NodeID: "node", CredentialRecipientKeyID: "key", CredentialRecipientPublicKey: make([]byte, 32)}
	err := service.attachCredentialLease(context.Background(), req, entry, "target", &iop.ProviderTunnelRequest{})
	if err == nil || !strings.Contains(err.Error(), "scope mismatch") {
		t.Fatalf("mismatched lease scope error = %v", err)
	}
}

func TestSubmitProviderPoolTunnelAttachesCredentialLeaseAfterCandidateSelection(t *testing.T) {
	env := newProviderTunnelTestEnv(t)
	provider := &fakeCredentialLeaseProvider{valid: true}
	env.svc.SetCredentialLeaseProvider(provider)
	env.svc.SetCredentialLeaseLimit(1)

	result, err := env.svc.SubmitProviderPool(context.Background(), ProviderPoolDispatchRequest{
		Run: SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true},
		Tunnel: SubmitProviderTunnelRequest{
			CredentialBinding: &CredentialBinding{
				PrincipalRef: "principal", CredentialSlotRef: "slot", RouteID: "route", ProfileID: "openai",
				CredentialRevision: 7, RouteRevision: 8, ProjectionGeneration: 9,
			},
			Method: "POST", Path: "/v1/chat/completions", Operation: "chat_completions",
			BuildBody: func(string) ([]byte, error) { return []byte(`{"model":"served-qwen"}`), nil },
		},
	})
	if err != nil {
		t.Fatalf("SubmitProviderPool: %v", err)
	}
	defer result.Tunnel.Close()
	waitForCondition(t, func() bool { return env.capturedRequest() != nil }, "fake node did not receive managed provider-pool tunnel")
	captured := env.capturedRequest()
	if captured.GetCredentialLease() == nil || captured.GetCredentialBinding() == nil {
		t.Fatal("managed provider-pool tunnel omitted dedicated credential fields")
	}
	if provider.acquireCalls != 1 || provider.gotBinding.GetNodeId() != "node-pool" || provider.gotBinding.GetRecipientKeyId() != "recipient-pool" || provider.gotBinding.GetUpstreamTarget() != "served-qwen" {
		t.Fatalf("lease acquisition did not use the selected candidate: %+v", provider.gotBinding)
	}
	if got := result.DispatchInfo; got.CredentialSlotRef != "slot" || got.CredentialRevision != 7 {
		t.Fatalf("dispatch credential attribution = %q/%d, want slot/7", got.CredentialSlotRef, got.CredentialRevision)
	}
	if got := result.Tunnel.Dispatch(); got.CredentialSlotRef != "slot" || got.CredentialRevision != 7 {
		t.Fatalf("tunnel credential attribution = %q/%d, want slot/7", got.CredentialSlotRef, got.CredentialRevision)
	}
}
