package verify

import (
	"context"
	"errors"
	"testing"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/pipeline"
)

// fakeOracle returns a scripted verdict and records how often it was probed.
type fakeOracle struct {
	name    string
	applies bool
	verdict OracleVerdict
	conf    float64
	proof   string
	err     error
	probes  int
}

func (f *fakeOracle) Name() string            { return f.name }
func (f *fakeOracle) Applies(c Candidate) bool { return f.applies }
func (f *fakeOracle) Probe(ctx context.Context, c Candidate) (OracleVerdict, float64, string, error) {
	f.probes++
	return f.verdict, f.conf, f.proof, f.err
}

// A decisive Proven oracle promotes the candidate to at least the proven floor
// WITHOUT ever paying for a model verification — external truth over model claim.
func TestDecide_OracleProvenBeatsVerifier(t *testing.T) {
	v := &fakeVerifier{real: false} // would REJECT if consulted
	o := &fakeOracle{name: "oob", applies: true, verdict: Proven, conf: 0.6, proof: "dns hit"}
	c := New(Policy{MaxVerifications: 10, MaxOracleProbes: 10}, v).WithOracles(o)

	d, p := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Promote {
		t.Fatalf("decision = %v, want Promote", d)
	}
	if p < provenFloor {
		t.Errorf("proven weight = %.2f, want >= %.2f (proof outranks model confidence 0.6)", p, provenFloor)
	}
	if o.probes != 1 {
		t.Errorf("oracle probes = %d, want 1", o.probes)
	}
	if v.calls != 0 {
		t.Errorf("a proven oracle must not spend a paid verification, got %d", v.calls)
	}
}

// A decisive Disproven oracle rejects the candidate and short-circuits the verifier.
func TestDecide_OracleDisprovenRejects(t *testing.T) {
	v := &fakeVerifier{real: true} // would PROMOTE if consulted
	o := &fakeOracle{name: "diff", applies: true, verdict: Disproven, proof: "identical to baseline"}
	c := New(Policy{MaxVerifications: 10, MaxOracleProbes: 10}, v).WithOracles(o)

	d, _ := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Reject {
		t.Fatalf("decision = %v, want Reject", d)
	}
	if v.calls != 0 {
		t.Errorf("a disproven oracle must not spend a paid verification, got %d", v.calls)
	}
}

// An Inconclusive oracle falls through to the paid model verifier.
func TestDecide_InconclusiveOracleFallsThroughToVerifier(t *testing.T) {
	v := &fakeVerifier{real: true, conf: 0.9}
	o := &fakeOracle{name: "oob", applies: true, verdict: Inconclusive}
	c := New(Policy{MaxVerifications: 10, MaxOracleProbes: 10}, v).WithOracles(o)

	d, _ := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Promote {
		t.Fatalf("decision = %v, want Promote (via verifier)", d)
	}
	if o.probes != 1 {
		t.Errorf("oracle should be probed once, got %d", o.probes)
	}
	if v.calls != 1 {
		t.Errorf("inconclusive oracle must fall through to the verifier, got %d calls", v.calls)
	}
}

// A probe error is treated as Inconclusive — infra failure neither confirms nor
// condemns; the verifier still runs.
func TestDecide_OracleErrorIsInconclusive(t *testing.T) {
	v := &fakeVerifier{real: true, conf: 0.9}
	o := &fakeOracle{name: "oob", applies: true, verdict: Proven, err: errors.New("tool missing")}
	c := New(Policy{MaxVerifications: 10, MaxOracleProbes: 10}, v).WithOracles(o)

	d, _ := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Promote || v.calls != 1 {
		t.Fatalf("probe error should fall through to verifier: decision=%v verifierCalls=%d", d, v.calls)
	}
}

// An oracle that does not apply is never probed; a later applicable one is.
func TestDecide_FirstDecisiveOracleWins(t *testing.T) {
	skip := &fakeOracle{name: "timing", applies: false, verdict: Proven}
	hit := &fakeOracle{name: "oob", applies: true, verdict: Proven, conf: 1, proof: "callback"}
	c := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(skip, hit)

	d, _ := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Promote {
		t.Fatalf("decision = %v, want Promote", d)
	}
	if skip.probes != 0 {
		t.Errorf("non-applicable oracle should not be probed, got %d", skip.probes)
	}
	if hit.probes != 1 {
		t.Errorf("applicable oracle probes = %d, want 1", hit.probes)
	}
}

// The oracle-probe cap is independent of the verification cap and is enforced.
func TestDecide_OracleProbeCapEnforced(t *testing.T) {
	o := &fakeOracle{name: "oob", applies: true, verdict: Inconclusive}
	c := New(Policy{MaxOracleProbes: 1}, nil).WithOracles(o)
	ctx := context.Background()

	// First high-stakes candidate spends the single probe.
	c.Decide(ctx, cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	// Second one finds the cap exhausted — no further probe.
	c.Decide(ctx, cand(pipeline.SeverityCritical, cheap), fakeEvidence{})

	if o.probes != 1 {
		t.Errorf("oracle probes = %d, want 1 (cap)", o.probes)
	}
	if c.OracleProbes() != 1 {
		t.Errorf("OracleProbes() = %d, want 1", c.OracleProbes())
	}
}

// With MaxOracleProbes=0, oracles are never consulted — opt-in by construction.
func TestDecide_ZeroProbeCapDisablesOracles(t *testing.T) {
	o := &fakeOracle{name: "oob", applies: true, verdict: Proven}
	v := &fakeVerifier{real: true, conf: 0.9}
	c := New(Policy{MaxVerifications: 10, MaxOracleProbes: 0}, v).WithOracles(o)

	c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if o.probes != 0 {
		t.Errorf("zero probe cap should disable oracles, got %d probes", o.probes)
	}
	if v.calls != 1 {
		t.Errorf("verifier should still run, got %d calls", v.calls)
	}
}

// Free corroboration still wins before any oracle probe is spent — the common,
// zero-cost case is unchanged by the oracle layer.
func TestDecide_FreeCorroborationStillPreemptsOracle(t *testing.T) {
	o := &fakeOracle{name: "oob", applies: true, verdict: Disproven}
	c := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(o)

	d, _ := c.Decide(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{toolGrounded: true})
	if d != Promote {
		t.Fatalf("tool-grounded decision = %v, want Promote", d)
	}
	if o.probes != 0 {
		t.Errorf("free corroboration must preempt the oracle probe, got %d", o.probes)
	}
}

// DecideWithProof carries the oracle receipt out when an oracle proves a
// candidate, so downstream can mark it VERIFIED and set edge exploitability.
func TestDecideWithProof_CarriesReceipt(t *testing.T) {
	o := &fakeOracle{name: "oob-callback", applies: true, verdict: Proven, conf: 1.0, proof: "dns from 203.0.113.9"}
	c := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(o)

	d, _, p := c.DecideWithProof(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Promote {
		t.Fatalf("decision = %v, want Promote", d)
	}
	if !p.Proven() || p.Oracle != "oob-callback" || p.Receipt != "dns from 203.0.113.9" || p.Confidence != 1.0 {
		t.Errorf("proof = %+v, want proven oob-callback receipt", p)
	}
}

// Disproven carries a proof too (so a report can explain the rejection).
func TestDecideWithProof_DisprovenCarriesProof(t *testing.T) {
	o := &fakeOracle{name: "differential-response", applies: true, verdict: Disproven, proof: "collapsed to control"}
	c := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(o)

	d, _, p := c.DecideWithProof(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if d != Reject {
		t.Fatalf("decision = %v, want Reject", d)
	}
	if p.Proven() || p.Verdict != Disproven || p.Oracle != "differential-response" {
		t.Errorf("proof = %+v, want disproven differential-response", p)
	}
}

// Non-oracle outcomes carry the zero Proof.
func TestDecideWithProof_NoOracleZeroProof(t *testing.T) {
	v := &fakeVerifier{real: true, conf: 0.9}
	c := New(Policy{MaxVerifications: 10}, v) // no oracles
	_, _, p := c.DecideWithProof(context.Background(), cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if p.Proven() || p.Verdict != Inconclusive || p.Oracle != "" {
		t.Errorf("proof = %+v, want zero value", p)
	}
}

func TestOracleVerdict_String(t *testing.T) {
	cases := map[OracleVerdict]string{Proven: "proven", Disproven: "disproven", Inconclusive: "inconclusive"}
	for v, want := range cases {
		if got := v.String(); got != want {
			t.Errorf("%d.String() = %q, want %q", v, got, want)
		}
		if v.Decisive() != (v != Inconclusive) {
			t.Errorf("%v.Decisive() wrong", v)
		}
	}
}

// Outcomes are tallied over multiple decisions.
func TestCorroborator_OracleOutcomes(t *testing.T) {
	provenO := &fakeOracle{name: "a", applies: true, verdict: Proven, conf: 1}
	c := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(provenO)
	ctx := context.Background()
	c.DecideWithProof(ctx, cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	c.DecideWithProof(ctx, cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if p, d := c.OracleOutcomes(); p != 2 || d != 0 {
		t.Errorf("outcomes = %d proven / %d disproven, want 2/0", p, d)
	}

	disO := &fakeOracle{name: "b", applies: true, verdict: Disproven}
	c2 := New(Policy{MaxOracleProbes: 10}, nil).WithOracles(disO)
	c2.DecideWithProof(ctx, cand(pipeline.SeverityCritical, cheap), fakeEvidence{})
	if p, d := c2.OracleOutcomes(); p != 0 || d != 1 {
		t.Errorf("outcomes = %d/%d, want 0/1", p, d)
	}
}
