package oracle

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/blackboard"
	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/verify"
)

// fakeSession scripts a payload URL/token and an observation outcome, and
// records the sequence of calls so tests can assert listen-before-deliver.
type fakeSession struct {
	url, token   string
	payloadErr   error
	hit          bool
	proof        string
	observeErr   error
	calls        []string
	observeToken string
}

func (s *fakeSession) Payload(ctx context.Context) (string, string, error) {
	s.calls = append(s.calls, "payload")
	return s.url, s.token, s.payloadErr
}

func (s *fakeSession) Observed(ctx context.Context, token string, window time.Duration) (bool, string, error) {
	s.calls = append(s.calls, "observed")
	s.observeToken = token
	return s.hit, s.proof, s.observeErr
}

func sqliCand() verify.Candidate {
	return verify.Candidate{Target: "https://t/api?id=1", Type: blackboard.TypePotentialSQLI, Detail: "time-based?"}
}

func TestOOB_Applies(t *testing.T) {
	o := NewOOB(&fakeSession{}, nil)
	cases := []struct {
		name string
		c    verify.Candidate
		want bool
	}{
		{"blind sqli type", sqliCand(), true},
		{"ssrf by detail", verify.Candidate{Type: blackboard.TypeCVEMatch, Detail: "possible SSRF to metadata"}, true},
		{"rce by detail", verify.Candidate{Type: blackboard.TypeExploitChain, Detail: "blind RCE via deserialization"}, true},
		{"plain misconfig", verify.Candidate{Type: blackboard.TypeMisconfig, Detail: "verbose errors"}, false},
	}
	for _, tc := range cases {
		if got := o.Applies(tc.c); got != tc.want {
			t.Errorf("%s: Applies = %v, want %v", tc.name, got, tc.want)
		}
	}
}

func TestOOB_NilSessionNeverApplies(t *testing.T) {
	o := NewOOB(nil, nil)
	if o.Applies(sqliCand()) {
		t.Error("a nil-session oracle must never apply")
	}
}

// A received callback is Proven with full confidence and carries the receipt.
func TestOOB_CallbackIsProven(t *testing.T) {
	s := &fakeSession{url: "http://abc.oast.fun", token: "abc", hit: true, proof: "http from 203.0.113.9"}
	var delivered string
	deliver := func(ctx context.Context, c verify.Candidate, oobURL string) error {
		delivered = oobURL
		return nil
	}
	o := NewOOB(s, deliver, WithWindow(time.Second))

	v, conf, proof, err := o.Probe(context.Background(), sqliCand())
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if v != verify.Proven || conf != 1.0 {
		t.Fatalf("verdict=%v conf=%v, want proven/1.0", v, conf)
	}
	if proof != "http from 203.0.113.9" {
		t.Errorf("proof = %q", proof)
	}
	if delivered != "http://abc.oast.fun" {
		t.Errorf("delivery got url %q, want the reserved OOB url", delivered)
	}
	// Listener must be armed before the payload is observed.
	if len(s.calls) != 2 || s.calls[0] != "payload" || s.calls[1] != "observed" {
		t.Errorf("call order = %v, want [payload observed]", s.calls)
	}
	if s.observeToken != "abc" {
		t.Errorf("observed token = %q, want the reserved token", s.observeToken)
	}
}

// Silence is Inconclusive — never Disproven (WAF/egress/slow callback all look
// like silence, so OOB cannot refute a finding).
func TestOOB_SilenceIsInconclusiveNotDisproven(t *testing.T) {
	s := &fakeSession{url: "http://x.oast.fun", token: "x", hit: false}
	o := NewOOB(s, func(context.Context, verify.Candidate, string) error { return nil }, WithWindow(time.Millisecond))

	v, _, _, err := o.Probe(context.Background(), sqliCand())
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if v != verify.Inconclusive {
		t.Errorf("silence verdict = %v, want inconclusive", v)
	}
	if v == verify.Disproven {
		t.Error("OOB must never disprove")
	}
}

// Delivery failure is an infra error (→ Inconclusive at the corroborator), and
// the listener is not observed after a failed send.
func TestOOB_DeliveryFailureIsError(t *testing.T) {
	s := &fakeSession{url: "http://x.oast.fun", token: "x", hit: true}
	o := NewOOB(s, func(context.Context, verify.Candidate, string) error { return errors.New("send failed") })

	v, _, _, err := o.Probe(context.Background(), sqliCand())
	if err == nil {
		t.Fatal("want delivery error")
	}
	if v != verify.Inconclusive {
		t.Errorf("verdict = %v, want inconclusive on delivery error", v)
	}
	for _, c := range s.calls {
		if c == "observed" {
			t.Error("must not observe after a failed delivery")
		}
	}
}

// Payload reservation failure surfaces as an error before any delivery.
func TestOOB_PayloadFailureIsError(t *testing.T) {
	s := &fakeSession{payloadErr: errors.New("interactsh unavailable")}
	delivered := false
	o := NewOOB(s, func(context.Context, verify.Candidate, string) error { delivered = true; return nil })

	_, _, _, err := o.Probe(context.Background(), sqliCand())
	if err == nil {
		t.Fatal("want payload error")
	}
	if delivered {
		t.Error("must not deliver when the listener could not be armed")
	}
}

// Listen-only mode (nil Delivery): the oracle just correlates a callback the
// exploit agent's own payloads triggered.
func TestOOB_ListenOnlyMode(t *testing.T) {
	s := &fakeSession{url: "http://x.oast.fun", token: "x", hit: true, proof: "dns"}
	o := NewOOB(s, nil, WithWindow(time.Millisecond))

	v, _, _, err := o.Probe(context.Background(), sqliCand())
	if err != nil || v != verify.Proven {
		t.Fatalf("listen-only: verdict=%v err=%v, want proven/nil", v, err)
	}
}

// The oracle satisfies verify.Oracle.
func TestOOB_ImplementsOracle(t *testing.T) {
	var _ verify.Oracle = NewOOB(&fakeSession{}, nil)
}
