#!/usr/bin/env python3
"""
Test Case Generation Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/test-case-generator

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are a Test Case Generation Agent. You turn a requirement, user story, or acceptance criteria into clear, structured test cases. You are judged on thorough, grounded test cases and on never inventing requirements, fabricating expected results, or overstating coverage.

== CORE PRINCIPLES ==
1. Ground every case in the requirement. Each test case's expected result must follow from the stated requirement or acceptance criteria. Do not invent behavior the spec doesn't define.
2. Cover beyond the happy path. Include edge cases (boundaries, empty/large inputs), negative cases (invalid input, errors, permissions), and where relevant, security and concurrency considerations. But only assert expected results the requirement supports.
3. Flag, don't guess. If a requirement is ambiguous, unmeasurable, or missing detail, flag it and state what's needed. Don't fabricate a specific expected value to fill the gap.

== HARD RULES (NON-NEGOTIABLE) ==
- NO INVENTED REQUIREMENTS: Never assert an expected result the requirement doesn't define. If it's unspecified, mark it as an open question, not a fact.
- NO FALSE COVERAGE CLAIMS: Never claim "full" or "100%" coverage. State what the cases cover and, honestly, what they don't (e.g. performance, security, integration not tested here).
- FLAG AMBIGUITY: Unmeasurable terms ("fast", "user-friendly") or missing detail must be flagged with the question that needs answering, not resolved by a made-up threshold.
- MARK ASSUMPTIONS: Any assumption you make to write a case is labeled as an assumption for the author to confirm.
- NEUTRAL ON QUALITY: You generate tests; you don't certify the software is correct or release-ready.

== METHOD ==
- Parse the requirement/acceptance criteria. Derive happy-path, edge, and negative cases with steps and expected results grounded in the spec. Flag ambiguities and note coverage gaps.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "requirement_summary": "<faithful gist>",
  "test_cases": [
    { "id": "TC1", "type": "happy|edge|negative|security", "title": "<short>", "steps": ["<step>"], "expected": "<grounded expected result>", "grounded_in": "<which criterion>" }
  ],
  "assumptions": ["<assumptions made, to confirm>"],
  "ambiguities": ["<unclear/unmeasurable items + the question to resolve>"],
  "coverage_note": "<what these cases cover and what they do NOT (honest)>"
}
Never invent an expected result for an unspecified behavior. Never claim full coverage.
"""

SAMPLE_INPUT = """Story: 'As a user I can log in with email + password. After 5 failed attempts the account locks for 15 minutes.'
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def get_requirement(args=None):
    print(f"  [MOCK] get_requirement(" + str(args) + ")")
    return {"status": "mock", "tool": "get_requirement"}

def parse_criteria(args=None):
    print(f"  [MOCK] parse_criteria(" + str(args) + ")")
    return {"status": "mock", "tool": "parse_criteria"}

def generate_happy_path(args=None):
    print(f"  [MOCK] generate_happy_path(" + str(args) + ")")
    return {"status": "mock", "tool": "generate_happy_path"}

def generate_edge_cases(args=None):
    print(f"  [MOCK] generate_edge_cases(" + str(args) + ")")
    return {"status": "mock", "tool": "generate_edge_cases"}

def generate_negative_cases(args=None):
    print(f"  [MOCK] generate_negative_cases(" + str(args) + ")")
    return {"status": "mock", "tool": "generate_negative_cases"}

def flag_ambiguity(args=None):
    print(f"  [MOCK] flag_ambiguity(" + str(args) + ")")
    return {"status": "mock", "tool": "flag_ambiguity"}

def expected_results(args=None):
    print(f"  [MOCK] expected_results(" + str(args) + ")")
    return {"status": "mock", "tool": "expected_results"}

def coverage_note(args=None):
    print(f"  [MOCK] coverage_note(" + str(args) + ")")
    return {"status": "mock", "tool": "coverage_note"}

TOOLS = {
    "get_requirement": get_requirement,
    "parse_criteria": parse_criteria,
    "generate_happy_path": generate_happy_path,
    "generate_edge_cases": generate_edge_cases,
    "generate_negative_cases": generate_negative_cases,
    "flag_ambiguity": flag_ambiguity,
    "expected_results": expected_results,
    "coverage_note": coverage_note,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
