"""CLI commands for VenomQA.

VenomQA CLI provides a comprehensive interface for:
- Running test journeys and exploring state graphs
- Managing test infrastructure with Docker
- Diagnosing issues with doctor command
- Running smoke tests for API validation

Exit Codes:
    0 (EXIT_SUCCESS): All operations completed successfully
    1 (EXIT_FAILURE): Tests failed or operation error
    2 (EXIT_CONFIG_ERROR): Configuration validation error

Environment Variables:
    VENOMQA_BASE_URL: Override API base URL
    VENOMQA_PROFILE: Configuration profile (dev, staging, prod)
    VENOMQA_VERBOSE: Enable verbose output (true/false)
    VENOMQA_TIMEOUT: HTTP timeout in seconds

Example Usage:
    venomqa init                    # Create new project
    venomqa run                     # Run all journeys
    venomqa run login_flow          # Run specific journey
    venomqa doctor                  # Check system health
    venomqa smoke-test http://api   # Quick API health check
"""

from __future__ import annotations

import logging
import sys
from datetime import datetime
from pathlib import Path
from typing import Any

import click

# Note: watchdog is imported lazily in the watch command to avoid import errors
# when watchdog is not installed
from venomqa.config import ConfigLoadError, ConfigValidationError, load_config
from venomqa.errors.debug import (
    DebugLevel,
    DebugLogger,
    StepThroughController,
)
from venomqa.runner import JourneyRunner

# Exit codes for CI/CD integration
# These are documented in the CLI help and used by CI systems
EXIT_SUCCESS = 0  # All journeys passed, operation successful
EXIT_FAILURE = 1  # Some journeys failed, tests did not pass
EXIT_CONFIG_ERROR = 2  # Configuration validation failed


def format_error_for_cli(error: Exception) -> str:
    """Format an error for CLI output with helpful suggestions.

    This function creates user-friendly error messages with:
    - Clear error description
    - Actionable suggestions when available
    - Documentation links

    Args:
        error: The exception to format

    Returns:
        Formatted error string suitable for CLI output
    """
    from venomqa.errors import VenomQAError

    lines = []

    if isinstance(error, VenomQAError):
        # Use the rich error formatting from VenomQAError
        lines.append(f"Error [{error.error_code.value}]: {error.message}")

        # Add location context
        location = error.context.format_location()
        if location != "unknown location":
            lines.append(f"Location: {location}")

        # Add suggestions
        if error.suggestions:
            lines.append("")
            lines.append("What to try:")
            for i, suggestion in enumerate(error.suggestions[:4], 1):
                lines.append(f"  {i}. {suggestion}")

        # Add documentation link
        lines.append("")
        lines.append(f"Documentation: {error.docs_url}")
    else:
        # Generic exception formatting
        lines.append(f"Error: {error}")
        lines.append("")
        lines.append("For help, run: venomqa doctor")

    return "\n".join(lines)

VENVOMQA_YAML_TEMPLATE = """# VenomQA Configuration
# Documentation: https://venomqa.dev/docs/configuration

# ============================================================================
# CRITICAL: VenomQA needs database rollback to explore state graphs
# ============================================================================
#
# VenomQA explores your API by:
#   1. Execute action → checkpoint database state
#   2. ROLLBACK database to checkpoint
#   3. Execute different action from same state
#   4. Repeat to explore ALL paths
#
# Without db_url, VenomQA can only test ONE linear path.
#
# Set up PostgreSQL:
#   $ docker run -d --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:15
#   $ export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/yourdb"
#
# Then uncomment db_url below and set it to your API's database.
# ============================================================================

# Target API configuration
base_url: "http://localhost:8000"
timeout: 30

# REQUIRED for state exploration - set this to your API's database
# db_url: "postgresql://user:pass@localhost:5432/testdb"
db_url: "${DATABASE_URL}"  # Uses environment variable

# Test execution settings
verbose: false
fail_fast: false
capture_logs: true
log_lines: 50

# Report settings
report_dir: "reports"

# Docker Compose file for test infrastructure (optional)
# docker_compose_file: "docker-compose.qa.yml"

# Notifications configuration (optional)
# notifications:
#   channels:
#     - type: slack
#       name: slack-qa
#       webhook_url: ${SLACK_WEBHOOK}
#       on: [failure, recovery]
#     - type: discord
#       name: discord-qa
#       webhook_url: ${DISCORD_WEBHOOK}
#       on: [failure]

# Port configurations for dependency injection (optional)
ports: []
  # - name: database
  #   adapter_type: postgres
  #   config:
  #     host: localhost
  #     port: 5432
  #     database: test_db
  # - name: time
  #   adapter_type: controllable_time
  # - name: cache
  #   adapter_type: redis
  #   config:
  #     host: localhost
  #     port: 6379
"""

DOCKER_COMPOSE_QA_TEMPLATE = """# VenomQA Docker Compose for QA environment
# Documentation: https://venomqa.dev/docs/docker

services:
  # Example: Test database
  # db:
  #   image: postgres:16
  #   environment:
  #     POSTGRES_USER: test
  #     POSTGRES_PASSWORD: test
  #     POSTGRES_DB: testdb
  #   ports:
  #     - "5432:5432"
  #   healthcheck:
  #     test: ["CMD-SHELL", "pg_isready -U test"]
  #     interval: 5s
  #     timeout: 5s
  #     retries: 5

  # Example: Your application under test
  # app:
  #   image: your-app:latest
  #   ports:
  #     - "8000:8000"
  #   environment:
  #     DATABASE_URL: postgresql://test:test@db:5432/testdb
  #   depends_on:
  #     db:
  #       condition: service_healthy

  # Example: Redis cache for testing
  # redis:
  #   image: redis:7-alpine
  #   ports:
  #     - "6379:6379"

  # Add your test services here
  placeholder:
    image: alpine:latest
    command: ["echo", "Add your services above"]
"""

ACTIONS_INIT_PY = '''"""Reusable actions for VenomQA tests (v1 API).

Actions are plain functions with signature (api, context).
  - api      : HttpClient  — use .get() .post() .put() .patch() .delete()
  - context  : Context     — use .get(key) / .set(key, val) — NOT context[key]

Example:
    from venomqa.v1 import Action

    def add_to_cart(api, context):
        product_id = context.get("product_id")
        resp = api.post("/api/cart/items", json={"product_id": product_id, "quantity": 1})
        context.set("cart_id", resp.json()["id"])
        return resp

    action = Action(name="add_to_cart", execute=add_to_cart, expected_status=[201])
"""
'''

FIXTURES_INIT_PY = '''"""Test fixtures for QA tests.

Fixtures provide test data and dependencies using dependency injection.
Use the @fixture decorator with optional `depends` for dependencies.

Example:
    from venomqa.plugins import fixture

    @fixture
    def db():
        from venomqa.adapters import get_adapter
        return get_adapter("postgres")(host="localhost", database="test")

    @fixture(depends=["db"])
    def user(db):
        return db.insert("users", {"email": "test@example.com", "name": "Test User"})
"""
'''

JOURNEYS_INIT_PY = '''"""VenomQA exploration definitions (v1 API).

Define actions and invariants, then run Agent.explore() to exhaustively
test every reachable state sequence — no linear test scripts needed.

Example:
    from venomqa.v1 import Action, Invariant, Agent, World, BFS, Severity
    from venomqa.v1.adapters.http import HttpClient

    def create_item(api, context):
        resp = api.post("/items", json={"name": "test"})
        context.set("item_id", resp.json()["id"])
        return resp

    def list_items(api, context):
        resp = api.get("/items")
        context.set("items", resp.json())
        return resp

    def list_is_valid(world):
        items = world.context.get("items") or []
        return isinstance(items, list)

    agent = Agent(
        world=World(api=HttpClient("http://localhost:8000")),
        actions=[
            Action(name="create_item", execute=create_item, expected_status=[201]),
            Action(name="list_items",  execute=list_items,  expected_status=[200]),
        ],
        invariants=[
            Invariant(name="list_valid", check=list_is_valid,
                      message="GET /items must return a list", severity=Severity.CRITICAL),
        ],
        strategy=BFS(),
        max_steps=200,
    )
    result = agent.explore()
"""
'''

SAMPLE_ACTION_PY = '''"""Sample actions for your VenomQA tests (v1 API).

Each action has signature: (api, context)
  - api      : HttpClient — .get() .post() .put() .patch() .delete()
  - context  : Context   — .get(key) / .set(key, val)  ← NOT context[key]

CRITICAL: Actions MUST validate responses using expect_* helpers:
  - resp.expect_status(201)         # raises if not 201
  - resp.expect_json_field("id")    # raises if field missing
  - resp.expect_json_list()         # raises if not array

Modify these for your specific API, then register them in an Agent.
"""


def health_check(api, context):
    """Check API health status."""
    resp = api.get("/health")
    resp.expect_status(200)  # raises AssertionError if not 200
    return resp


def list_items(api, context):
    """List all items and store in context.

    Uses expect_json_list() to validate the response is an array.
    """
    resp = api.get("/api/items")
    resp.expect_status(200)
    items = resp.expect_json_list()  # raises if not a list

    context.set("items", items)
    context.set("item_count", len(items))
    return resp


def create_item(api, context):
    """Create a new item and store its ID in context.

    Uses expect_json_field() to validate required fields exist.
    """
    resp = api.post("/api/items", json={
        "name": "VenomQA Test Item",
        "description": "Created by VenomQA",
    })
    resp.expect_status(200, 201)                    # 200 or 201
    data = resp.expect_json_field("id")             # raises if "id" missing

    context.set("item_id", data["id"])
    return resp


def get_item(api, context):
    """Fetch a single item by ID.

    GOOD PATTERN: Use Action(preconditions=["create_item"]) instead of
    checking context.has() inside the action.
    """
    item_id = context.get("item_id")
    resp = api.get(f"/api/items/{item_id}")
    resp.expect_status(200)

    data = resp.expect_json()
    if data.get("id") != item_id:
        raise AssertionError(f"Wrong item returned: expected {item_id}, got {data}")

    return resp


def delete_item(api, context):
    """Delete the item created by create_item.

    GOOD PATTERN: This action should use preconditions=["create_item"]
    so VenomQA only runs it after create_item has succeeded.

    BAD PATTERN (DON'T DO THIS):
        if item_id is None:
            return api.get("/noop")   # Silent no-op - hides bugs!
    """
    item_id = context.get("item_id")
    resp = api.delete(f"/api/items/{item_id}")
    resp.expect_status(200, 204)  # raises if not 200 or 204

    # Clean up context so we know item is gone
    context.delete("item_id")
    return resp
'''

def _get_readme_template(base_path: str) -> str:
    """Generate README content for an initialized project."""
    return f'''# VenomQA Test Suite

Autonomous API exploration — define actions and invariants, let VenomQA find every bug sequence.

## Directory Structure

```
{base_path}/
|-- venomqa.yaml              # API URL and settings
|-- llm-context.md            # Paste this into any AI assistant for help
|-- actions/                  # Your action functions
|   +-- __init__.py
|   +-- sample_actions.py     # (if --with-sample)
|-- journeys/                 # Exploration scripts
|   +-- __init__.py
|   +-- sample_journey.py     # (if --with-sample)
|-- fixtures/                 # Shared test data
|   +-- __init__.py
+-- reports/                  # Generated reports
```

## Quick Start

**CRITICAL**: VenomQA needs database rollback to explore branches.

1. **Identify your API's database** — PostgreSQL, SQLite, or none?
2. **Edit `venomqa.yaml`** — set `base_url` AND `db_url` (same DB your API uses)
3. **Write actions** in `actions/` — signature: `def my_action(api, context)`
4. **Run an exploration**:
   ```bash
   python3 journeys/sample_journey.py
   ```

Why database access is required: VenomQA explores by branching. To test both
`create → update` AND `create → delete` from the same state, it must:
1. Create → reach state S1
2. Update → explore branch A
3. **ROLLBACK to S1** ← requires database access
4. Delete → explore branch B

## Actions (v1 API)

Actions MUST validate responses. Don't assume success!

```python
# actions/my_actions.py

def create_user(api, context):
    resp = api.post("/users", json={{"name": "Alice"}})

    # GOOD: Validate before using response
    if resp.status_code != 201:
        raise AssertionError(f"Create failed: {{resp.status_code}} - {{resp.text}}")
    data = resp.json()
    if "id" not in data:
        raise AssertionError(f"Missing 'id' in response: {{data}}")

    context.set("user_id", data["id"])   # ← .set(), not context["key"] =
    return resp

def get_user(api, context):
    user_id = context.get("user_id")     # ← .get(), not context["key"]
    resp = api.get(f"/users/{{user_id}}")

    if resp.status_code != 200:
        raise AssertionError(f"Get user failed: {{resp.status_code}}")
    return resp
```

## Invariants

Invariants should make LIVE API calls to verify server state, not just check context.

```python
from venomqa.v1 import Invariant, Severity

def users_list_is_valid(world):
    # GOOD: Make a live API call to verify server state
    resp = world.api.get("/users")
    if resp.status_code != 200:
        return False
    data = resp.json()
    return isinstance(data, list)   # Must be a list

def user_exists_on_server(world):
    # GOOD: Cross-check client state against server
    user_id = world.context.get("user_id")
    if user_id is None:
        return True   # Nothing to check yet
    resp = world.api.get(f"/users/{{user_id}}")
    return resp.status_code == 200

Invariant(
    name="users_list_valid",
    check=users_list_is_valid,
    message="GET /users must return valid JSON array",   # ← 'message', not 'description'
    severity=Severity.CRITICAL,
)
```

## Run an exploration

**CRITICAL**: VenomQA needs database rollback to explore branches.
Without it, VenomQA can only test ONE linear path.

```python
import os
from venomqa.v1 import Action, Invariant, Agent, World, DFS, Severity
from venomqa.v1.adapters.http import HttpClient
from venomqa.v1.adapters.postgres import PostgresAdapter  # or SQLiteAdapter
from actions.my_actions import create_user, get_user

# Connect to the SAME database your API writes to
api = HttpClient("http://localhost:8000")
db = PostgresAdapter(os.environ["DATABASE_URL"])

agent = Agent(
    world=World(api=api, systems={{"db": db}}),  # ← REQUIRED for state exploration
    actions=[
        Action(name="create_user", execute=create_user, expected_status=[201]),
        Action(name="get_user",    execute=get_user,    expected_status=[200],
               preconditions=["create_user"]),  # ← only run after create_user
    ],
    invariants=[...],
    strategy=DFS(),   # ← use DFS with PostgreSQL
    max_steps=200,
)
result = agent.explore()
```

**No database?** Use `state_from_context=["user_id", ...]` for limited context-based exploration.

## Using an AI assistant

See `llm-context.md` in this directory — paste it into Claude, ChatGPT, or
Cursor so the AI knows the exact VenomQA API and won\'t give you wrong code.

Or regenerate it any time:
```bash
venomqa llm-docs -o llm-context.md
```

## How State Traversal Works

VenomQA explores your API by trying every possible sequence of actions,
using checkpoints to branch and rollback between paths:

```
                    [Initial State]
                          |
         +----------------+----------------+
         |                |                |
     create_user      list_items      delete_item
         |                |                |
    [Checkpoint]     [Checkpoint]    (no-op: nothing to delete)
         |                |
    +----+----+      +----+----+
    |         |      |         |
create_repo  login  create   list
    |         |     _item    _items
   ...       ...     |         |
                [Checkpoint]   ...
                    |
              +-----+-----+
              |           |
          delete_item  update_item
              |           |
             ...         ...
```

**Key concepts:**

1. **BFS (Breadth-First Search)**: Explores all 1-action sequences, then all
   2-action sequences, etc. Finds shallow bugs fast.

2. **Checkpoints**: Before branching, VenomQA saves the current state (database
   snapshot, context values, etc.)

3. **Rollback**: After exploring one branch, VenomQA rolls back to the checkpoint
   and tries the next action — so each path starts from the same state.

4. **Invariants**: Checked after EVERY action. If any invariant returns False,
   VenomQA records a **violation** with the exact reproduction path.

**Example exploration:**
```
Step 1: create_user                     → check invariants
Step 2: create_user → create_repo       → check invariants
Step 3: create_user → login             → check invariants
Step 4: create_user → create_repo → ... → check invariants
```

## Understanding Violations

When a violation is found, VenomQA shows:

```
[CRITICAL] refund_cannot_exceed_payment
  Message: Refunded amount exceeds original payment. Billing bug!

  Reproduction Path:
    -> create_customer
    -> create_payment_intent
    -> confirm_payment
    -> create_refund            ← violation triggered here

  Request: POST http://localhost:8000/refunds
  Response: 200
  Body: {{"amount": 2000, ...}}
```

**Severity levels:**
- `CRITICAL`: Data corruption, security breach, money issues
- `HIGH`: Major feature broken
- `MEDIUM`: Partial functionality loss
- `LOW`: Minor issues

## Common mistakes

| Wrong | Correct |
|-------|---------|
| `def action(ctx, api)` | `def action(api, context)` |
| `context["key"] = val` | `context.set("key", val)` |
| `Invariant(description=...)` | `Invariant(message=...)` |
| `def check(state, ctx)` | `def check(world)` |
| `reporter.report(result, path=...)` | `open(f).write(reporter.report(result))` |
'''

SAMPLE_JOURNEY_PY = '''"""Sample VenomQA exploration (v1 API).

Demonstrates basic CRUD exploration — VenomQA tries every sequence of
actions and checks the invariants after each step.

CRITICAL: VenomQA needs database rollback to explore state graphs.
Without it, VenomQA can only test ONE linear path. See options below.

Run with: python3 sample_journey.py
"""

import os
import sys

from venomqa.v1 import Action, Invariant, Agent, World, BFS, DFS, Severity
from venomqa.v1.adapters.http import HttpClient

from actions.sample_actions import (
    health_check, list_items, create_item, get_item, delete_item
)


# --- Invariants: rules that must always hold -----------------------------------

def list_always_returns_array(world):
    """GET /api/items must always return a JSON array.

    GOOD PATTERN: Make a live API call to verify server state.
    Don't just check context - the server might be inconsistent.
    """
    resp = world.api.get("/api/items")
    if resp.status_code != 200:
        return False  # Invariant failed: API is broken
    data = resp.json()
    return isinstance(data, list)


def item_count_matches_server(world):
    """Context item_count must match actual server count.

    GOOD PATTERN: Cross-check client state against server state.
    This catches bugs where the client thinks something happened but it didn't.
    """
    expected = world.context.get("item_count")
    if expected is None:
        return True  # list_items hasn't run yet

    resp = world.api.get("/api/items")
    if resp.status_code != 200:
        return False
    actual = len(resp.json())
    return actual == expected


def deleted_item_not_retrievable(world):
    """After delete, the item should return 404.

    GOOD PATTERN: Test negative conditions, not just positive.
    """
    # Only check if we just deleted something
    if world.context.has("item_id"):
        return True  # Item still exists, nothing to check

    # If we had an item_id but it's now gone, verify server agrees
    deleted_id = world.context.get("_last_deleted_id")
    if deleted_id is None:
        return True

    resp = world.api.get(f"/api/items/{deleted_id}")
    return resp.status_code == 404


# --- Database setup ------------------------------------------------------------
#
# VenomQA explores state graphs by ROLLING BACK the database after each branch.
# You MUST connect to the SAME database your API uses.
#
# Choose ONE of these options:

def setup_with_postgres():
    """Option 1: PostgreSQL (most common for production APIs)"""
    from venomqa.v1.adapters.postgres import PostgresAdapter

    db_url = os.environ.get("DATABASE_URL")
    if not db_url:
        print("ERROR: Set DATABASE_URL to your PostgreSQL connection string")
        print("  export DATABASE_URL='postgresql://user:pass@localhost:5432/yourdb'")
        sys.exit(1)

    api = HttpClient("http://localhost:8000")
    db = PostgresAdapter(db_url)

    # CRITICAL: use DFS() with PostgreSQL (BFS doesn't work with PG savepoints)
    return World(api=api, systems={"db": db}), DFS()


def setup_with_sqlite():
    """Option 2: SQLite (simpler, works with BFS)"""
    from venomqa.v1.adapters.sqlite import SQLiteAdapter

    db_path = os.environ.get("SQLITE_PATH", "/path/to/your/api.db")

    api = HttpClient("http://localhost:8000")
    db = SQLiteAdapter(db_path)

    return World(api=api, systems={"db": db}), BFS()


def setup_with_context_only():
    """Option 3: No database - uses context for state identity.

    WARNING: This is a LIMITED mode for APIs that don't have a database.
    State exploration is based on context values only, not actual DB state.
    Use this only if your API is stateless or you're just testing the basics.
    """
    api = HttpClient("http://localhost:8000")

    # Track these context keys to distinguish states
    # Each unique combination = a different state
    return World(
        api=api,
        state_from_context=["item_id", "item_count"],  # context-based state
    ), BFS()


# --- Run exploration -----------------------------------------------------------

if __name__ == "__main__":
    # CHANGE THIS: Pick your setup based on your database
    # world, strategy = setup_with_postgres()
    # world, strategy = setup_with_sqlite()
    world, strategy = setup_with_context_only()  # Default: limited mode

    agent = Agent(
        world=world,
        actions=[
            Action(name="health_check", execute=health_check, expected_status=[200]),
            Action(name="list_items",   execute=list_items,   expected_status=[200]),
            Action(name="create_item",  execute=create_item,  expected_status=[200, 201]),
            Action(
                name="get_item",
                execute=get_item,
                expected_status=[200],
                preconditions=["create_item"],  # Only run after create_item
            ),
            Action(
                name="delete_item",
                execute=delete_item,
                preconditions=["create_item"],  # Only run after create_item
            ),
        ],
        invariants=[
            Invariant(
                name="list_returns_array",
                check=list_always_returns_array,
                message="GET /api/items must return JSON array",
                severity=Severity.CRITICAL,
            ),
            Invariant(
                name="count_matches_server",
                check=item_count_matches_server,
                message="Client item_count must match server",
                severity=Severity.HIGH,
            ),
            Invariant(
                name="deleted_is_404",
                check=deleted_item_not_retrievable,
                message="Deleted items must return 404",
                severity=Severity.HIGH,
            ),
        ],
        strategy=strategy,
        max_steps=200,
        progress_every=20,  # Print progress every 20 steps
    )

    print("Starting exploration...")
    print(f"  Strategy: {type(strategy).__name__}")
    print(f"  Max steps: {agent.max_steps}")
    print()

    result = agent.explore()

    print()
    print("=" * 60)
    print("RESULTS")
    print("=" * 60)
    print(f"  States visited    : {result.states_visited}")
    print(f"  Transitions       : {result.transitions_taken}")
    print(f"  Action coverage   : {result.action_coverage_percent:.0f}%")
    print(f"  Duration          : {result.duration_ms:.0f}ms")
    print(f"  Violations        : {len(result.violations)}")

    if result.violations:
        print()
        print("VIOLATIONS FOUND:")
        for v in result.violations:
            print(f"  [{v.severity.value.upper()}] {v.invariant_name}")
            print(f"      {v.message}")
    else:
        print()
        print("All invariants passed.")

    # Exit with error if violations found (useful for CI)
    sys.exit(1 if result.violations else 0)
'''


def setup_logging(verbose: bool) -> None:
    """Configure logging based on verbosity level."""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )


def discover_journeys() -> dict[str, Any]:
    """Discover available journeys from the journeys directory."""
    journeys: dict[str, Any] = {}
    journeys_dir = Path("journeys")

    if not journeys_dir.exists():
        return journeys

    for journey_file in journeys_dir.glob("*.py"):
        if journey_file.name.startswith("_"):
            continue
        journey_name = journey_file.stem
        journeys[journey_name] = {
            "name": journey_name,
            "path": str(journey_file),
        }

    return journeys


def get_version() -> str:
    """Get the VenomQA version string."""
    try:
        from venomqa import __version__
        return f"VenomQA {__version__}"
    except ImportError:
        return "VenomQA (version unknown)"


EPILOG = """\b
Examples:
  venomqa init --with-sample      Create project + sample exploration
  venomqa demo                    Run built-in demo (no setup needed)
  venomqa llm-docs                Print AI assistant context doc
  venomqa doctor                  Check system dependencies
  venomqa smoke-test URL          Quick API health check

Exit Codes:
  0  Success
  1  Test failures
  2  Configuration error

Documentation: https://github.com/namanag97/venomqa
"""


@click.group(invoke_without_command=True, epilog=EPILOG)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output with detailed logging")
@click.option("--config", "-c", type=click.Path(exists=True), help="Path to config file (default: venomqa.yaml)")
@click.option("--profile", "-p", type=str, help="Configuration profile to use (dev, staging, prod)")
@click.version_option(version=None, prog_name="VenomQA", message=get_version())
@click.pass_context
def cli(ctx: click.Context, verbose: bool, config: str | None, profile: str | None) -> None:
    """Autonomous API QA agent — exhaustively explore every action sequence.

    Define actions (what to call) and invariants (rules that must hold), then
    let VenomQA find every bug sequence your linear tests miss.

    \b
    Quick Start:
      venomqa demo                    See it in action (no setup needed)
      venomqa init --with-sample      Create a project with sample files
      venomqa llm-docs                Get AI assistant context doc
      venomqa doctor                  Check your environment

    Run 'venomqa COMMAND --help' for command-specific help.
    """
    ctx.ensure_object(dict)

    # Skip config loading for commands that don't need it, and whenever
    # --help is requested (config errors must never block help text).
    help_requested = "--help" in sys.argv or "-h" in sys.argv
    no_config_commands = {"init", "doctor", "smoke-test", "demo", "llm-docs"}
    if ctx.invoked_subcommand in no_config_commands or help_requested:
        ctx.obj["verbose"] = verbose
        setup_logging(verbose)
        return

    # Show help if no command given
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())
        return

    try:
        config_obj = load_config(config, profile=profile)
    except (ConfigLoadError, ConfigValidationError) as e:
        # Use the new error formatter for better CLI output
        click.echo(format_error_for_cli(e), err=True)

        # Additional config-specific help
        click.echo("\nQuick fixes:", err=True)
        click.echo("  - Run 'venomqa validate' to check your configuration", err=True)
        click.echo("  - Run 'venomqa init' to create a new config file", err=True)

        sys.exit(EXIT_CONFIG_ERROR)

    if verbose:
        config_obj["verbose"] = True

    ctx.obj["config"] = config_obj
    ctx.obj["verbose"] = verbose
    ctx.obj["profile"] = profile

    setup_logging(verbose)


# Register history subcommands
from venomqa.cli.history import history

cli.add_command(history)

# Register doctor command
from venomqa.cli.doctor import doctor

cli.add_command(doctor)

# Register docker subcommands
from venomqa.cli.docker import docker

cli.add_command(docker)

# Register demo command
from venomqa.cli.demo import demo

cli.add_command(demo)

# Register llm-docs command
from venomqa.cli.llm_docs import llm_docs

cli.add_command(llm_docs)


@cli.command()
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
def preflight(ctx: click.Context, output_json: bool) -> None:
    """Run preflight checks before test execution.

    This command verifies that the environment is properly configured
    for running tests:
    - Target API is reachable
    - Docker services are running (if configured)
    - Database is reachable (if configured)
    - Configuration is valid
    - Journeys directory exists

    Exit codes:
        0 - All required checks passed
        1 - One or more required checks failed
    """
    import json as json_module

    from venomqa.preflight import run_preflight_checks, run_preflight_checks_with_output

    config: dict[str, Any] = ctx.obj.get("config", {})

    if output_json:
        result = run_preflight_checks(config)
        click.echo(json_module.dumps(result.to_dict(), indent=2))
        sys.exit(0 if result.success else 1)
    else:
        from rich.console import Console

        console = Console()
        result = run_preflight_checks_with_output(config, console)
        sys.exit(0 if result.success else 1)


@cli.command("smoke-test")
@click.argument("base_url", required=False, default=None)
@click.option("--config", "-c", "config_file", default=None, help="YAML config file for smoke tests")
@click.option("--init", "init_config", is_flag=True, help="Print an example config file to stdout")
@click.option("--token", "-t", default=None, help="Bearer token for authenticated checks")
@click.option("--base-url", "base_url_override", default=None, help="Override base URL from config")
@click.option("--health-path", default="/health", help="Health endpoint path")
@click.option("--auth-path", default="/api/v1/workspaces", help="Auth-protected endpoint path")
@click.option("--create-path", default=None, help="POST endpoint to test resource creation")
@click.option("--create-payload", default=None, help="JSON payload for create check")
@click.option("--list-path", default=None, help="GET endpoint to test list/pagination")
@click.option("--timeout", default=10.0, type=float, help="HTTP timeout in seconds")
@click.option("--json", "output_json", is_flag=True, help="Output results as JSON")
@click.option(
    "--auto-openapi",
    default=None,
    help="Auto-discover checks from OpenAPI spec URL",
)
def smoke_test(
    base_url: str | None,
    config_file: str | None,
    init_config: bool,
    token: str | None,
    base_url_override: str | None,
    health_path: str,
    auth_path: str,
    create_path: str | None,
    create_payload: str | None,
    list_path: str | None,
    timeout: float,
    output_json: bool,
    auto_openapi: str | None,
) -> None:
    """Run a quick smoke test against an API before full test suites.

    Validates that the API is minimally functional by checking health,
    authentication, and optionally resource creation and listing.

    Supports three modes:
    1. Config file: --config preflight.yaml
    2. Command-line args: venomqa smoke-test http://localhost:8000
    3. Auto-discovery: --auto-openapi /openapi.json

    \b
    Examples:
        venomqa smoke-test http://localhost:8000
        venomqa smoke-test --config preflight.yaml
        venomqa smoke-test --config preflight.yaml --base-url http://staging:8000
        venomqa smoke-test http://localhost:8000 --token $TOKEN
        venomqa smoke-test http://localhost:8000 --auto-openapi /openapi.json
        venomqa smoke-test --init > preflight.yaml

    Exit codes:
        0 - All checks passed, API is ready
        1 - One or more checks failed
    """
    import json as json_module

    # --init mode: print example config and exit
    if init_config:
        from venomqa.preflight.config import generate_example_config

        click.echo(generate_example_config())
        sys.exit(EXIT_SUCCESS)

    # Config file mode
    if config_file:
        from venomqa.preflight import PreflightConfig, SmokeTest

        try:
            config = PreflightConfig.from_yaml(config_file)
        except FileNotFoundError:
            click.echo(f"Error: Config file not found: {config_file}", err=True)
            sys.exit(EXIT_CONFIG_ERROR)
        except Exception as e:
            click.echo(f"Error loading config: {e}", err=True)
            sys.exit(EXIT_CONFIG_ERROR)

        # Apply CLI overrides
        if base_url_override:
            config.base_url = base_url_override
        elif base_url:
            config.base_url = base_url
        if token:
            config.token = token

        smoke = SmokeTest.from_config(config)
        report = smoke.run_all()

        if output_json:
            click.echo(json_module.dumps(report.to_dict(), indent=2))
        else:
            report.print_report()

        sys.exit(EXIT_SUCCESS if report.passed else EXIT_FAILURE)

    # Non-config modes require a base_url argument
    if not base_url:
        click.echo(
            "Error: BASE_URL is required unless using --config or --init.",
            err=True,
        )
        sys.exit(EXIT_CONFIG_ERROR)

    # Parse create payload from JSON string
    parsed_payload: dict[str, Any] | None = None
    if create_payload:
        try:
            parsed_payload = json_module.loads(create_payload)
        except json_module.JSONDecodeError as e:
            click.echo(f"Error: Invalid JSON in --create-payload: {e}", err=True)
            sys.exit(EXIT_CONFIG_ERROR)

    # Auto-discover mode
    if auto_openapi:
        from venomqa.preflight import AutoPreflight

        spec_url = auto_openapi
        if not spec_url.startswith("http"):
            spec_url = f"{base_url.rstrip('/')}{spec_url}"

        try:
            auto = AutoPreflight.from_openapi(
                spec_url=spec_url,
                token=token,
                timeout=timeout,
            )
            report = auto.run()
        except Exception as e:
            click.echo(f"Error fetching OpenAPI spec: {e}", err=True)
            sys.exit(EXIT_FAILURE)
    else:
        from venomqa.preflight import SmokeTest

        smoke = SmokeTest(base_url=base_url, token=token, timeout=timeout)
        report = smoke.run_all(
            health_path=health_path,
            auth_path=auth_path,
            create_path=create_path,
            create_payload=parsed_payload,
            list_path=list_path,
        )

    if output_json:
        click.echo(json_module.dumps(report.to_dict(), indent=2))
    else:
        report.print_report()

    sys.exit(EXIT_SUCCESS if report.passed else EXIT_FAILURE)


@cli.command()
@click.argument("journey_names", nargs=-1)
@click.option("--no-infra", is_flag=True, help="Skip infrastructure setup/teardown")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.option("--fail-fast", is_flag=True, help="Stop on first failure")
@click.option(
    "--port",
    "ports",
    multiple=True,
    help="Port configuration in format: name=adapter_type (e.g., database=postgres)",
)
@click.option(
    "--benchmark",
    is_flag=True,
    help="Run in benchmark mode with performance metrics",
)
@click.option(
    "--benchmark-iterations",
    type=int,
    default=10,
    help="Number of benchmark iterations (default: 10)",
)
@click.option(
    "--parallel",
    "-p",
    type=int,
    default=1,
    help="Number of parallel journeys to run (default: 1)",
)
@click.option(
    "--debug",
    "-d",
    is_flag=True,
    help="Enable debug mode with detailed logging of HTTP requests, SQL queries, and timing",
)
@click.option(
    "--debug-log",
    type=click.Path(),
    default=None,
    help="Path to save debug log file (default: logs/debug-<timestamp>.log)",
)
@click.option(
    "--step",
    "step_through",
    is_flag=True,
    help="Enable step-through mode - pause after each step for inspection",
)
@click.option(
    "--notify-on-failure",
    is_flag=True,
    help="Send notifications only on failure",
)
@click.option(
    "--notify-always",
    is_flag=True,
    help="Send notifications on both success and failure",
)
@click.option(
    "--report-url",
    type=str,
    default=None,
    help="URL to include in notifications linking to the full report",
)
@click.pass_context
def run(
    ctx: click.Context,
    journey_names: tuple[str, ...],
    no_infra: bool,
    output_format: str,
    fail_fast: bool,
    ports: tuple[str, ...],
    benchmark: bool,
    benchmark_iterations: int,
    parallel: int,
    debug: bool,
    debug_log: str | None,
    step_through: bool,
    notify_on_failure: bool,
    notify_always: bool,
    report_url: str | None,
) -> None:
    """Run journeys (optionally filtered by name).

    If no journey names are provided, runs all discovered journeys.

    Use --benchmark to run journeys multiple times and collect performance
    metrics including steps/second and journeys/minute.

    Use --parallel to run multiple journeys concurrently (each with its
    own isolated client and state).

    Use --debug to enable detailed logging of HTTP requests/responses,
    SQL queries, and timing breakdowns. Logs are saved to a file.

    Use --step to pause after each step for manual inspection.
    Type 'help' at the prompt for available commands.
    """
    config: dict[str, Any] = ctx.obj["config"]
    config["fail_fast"] = fail_fast

    parsed_ports = _parse_ports(ports, config)

    journeys = discover_journeys()

    if not journeys:
        click.echo("No journeys found. Create journeys in the 'journeys/' directory.", err=True)
        sys.exit(1)

    to_run = list(journey_names) if journey_names else list(journeys.keys())

    for name in to_run:
        if name not in journeys:
            click.echo(f"Journey not found: {name}", err=True)
            sys.exit(1)

    from venomqa.cli.output import CLIOutput, ProgressConfig

    output = CLIOutput(ProgressConfig())

    # Benchmark mode
    if benchmark:
        output.console.print(
            f"\n[bold]Benchmarking {len(to_run)} journey(s) "
            f"({benchmark_iterations} iterations each)...[/bold]"
        )
        _run_benchmark_mode(
            to_run, journeys, config, parsed_ports, benchmark_iterations, output, output_format
        )
        return

    # Parallel execution mode
    if parallel > 1:
        output.console.print(
            f"\n[bold]Running {len(to_run)} journey(s) with {parallel} parallel workers...[/bold]"
        )
        _run_parallel_mode(
            to_run, journeys, config, no_infra, parsed_ports, parallel, output, output_format, ctx
        )
        return

    # Configure debug logger if debug mode is enabled
    debug_logger_instance: DebugLogger | None = None
    if debug:
        if debug_log is None:
            # Generate default log path with timestamp
            log_dir = Path("logs")
            log_dir.mkdir(exist_ok=True)
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            debug_log = str(log_dir / f"debug-{timestamp}.log")

        debug_logger_instance = DebugLogger.configure(
            enabled=True,
            log_file=debug_log,
            level=DebugLevel.VERBOSE if ctx.obj.get("verbose") else DebugLevel.DETAILED,
        )
        output.console.print(f"[dim]Debug logging enabled. Log file: {debug_log}[/dim]")

    # Configure step-through controller if step mode is enabled
    step_controller: StepThroughController | None = None
    if step_through:
        step_controller = StepThroughController(enabled=True)
        output.console.print("[dim]Step-through mode enabled. Press Enter to continue after each step.[/dim]")

    # Store debug/step config for journey execution
    config["debug_logger"] = debug_logger_instance
    config["step_controller"] = step_controller

    # Standard sequential mode
    output.console.print(f"\n[bold]Running {len(to_run)} journey(s)...[/bold]")

    results: list[dict[str, Any]] = []
    all_passed = True

    try:
        for journey_name in to_run:
            journey_data = _load_journey(journey_name, journeys[journey_name]["path"])
            if journey_data is None:
                output.console.print(
                    f"[red]x Failed to load journey: {journey_name}[/red]", err=True
                )
                all_passed = False
                continue

            result = _execute_journey(journey_data, config, no_infra, parsed_ports)
            results.append(result)

            all_passed = all_passed and result.get("success", False)

            if not result.get("success") and fail_fast:
                output.console.print("\n[yellow]Fail-fast triggered, stopping.[/yellow]")
                break

    finally:
        # Close debug logger
        if debug_logger_instance:
            debug_logger_instance.close()
            output.console.print(f"[dim]Debug log saved to: {debug_log}[/dim]")

    ctx.obj["last_results"] = results

    # Handle notifications
    if notify_on_failure or notify_always:
        _send_notifications(
            results=results,
            config=config,
            notify_on_failure=notify_on_failure,
            notify_always=notify_always,
            report_url=report_url,
            all_passed=all_passed,
            output=output,
        )

    if output_format == "json":
        import json

        click.echo(json.dumps(results, indent=2, default=str))
    else:
        _print_summary(results)

    sys.exit(EXIT_SUCCESS if all_passed else EXIT_FAILURE)


def _run_benchmark_mode(
    to_run: list[str],
    journeys: dict[str, Any],
    config: dict[str, Any],
    ports: dict[str, Any],
    iterations: int,
    output: Any,
    output_format: str,
) -> None:
    """Run journeys in benchmark mode."""
    from venomqa import Client
    from venomqa.performance.benchmark import BenchmarkConfig, BenchmarkSuite

    suite = BenchmarkSuite(
        BenchmarkConfig(
            iterations=iterations,
            warmup_iterations=max(1, iterations // 10),
            verbose=False,
        )
    )

    for journey_name in to_run:
        journey_data = _load_journey(journey_name, journeys[journey_name]["path"])
        if journey_data is None:
            output.console.print(f"[red]x Failed to load journey: {journey_name}[/red]", err=True)
            continue

        def runner_factory(j=journey_data):
            client = Client(
                base_url=config.get("base_url", "http://localhost:8000"),
                timeout=config.get("timeout", 30),
            )
            return JourneyRunner(
                client=client,
                fail_fast=False,
                capture_logs=False,
                ports=ports,
            )

        suite.add(journey_data, runner_factory)

    results = suite.run_all()

    if output_format == "json":
        import json

        data = [r.to_dict() for r in results]
        click.echo(json.dumps(data, indent=2))
    else:
        output.console.print(suite.get_report())

    sys.exit(0)


def _run_parallel_mode(
    to_run: list[str],
    journeys: dict[str, Any],
    config: dict[str, Any],
    no_infra: bool,
    ports: dict[str, Any],
    parallel: int,
    output: Any,
    output_format: str,
    ctx: click.Context,
) -> None:
    """Run journeys in parallel mode."""
    from venomqa import Client
    from venomqa.performance.optimizations import ParallelJourneyExecutor

    # Load all journeys first
    journey_objects = []
    for journey_name in to_run:
        journey_data = _load_journey(journey_name, journeys[journey_name]["path"])
        if journey_data is None:
            output.console.print(f"[red]x Failed to load journey: {journey_name}[/red]", err=True)
            continue
        journey_objects.append(journey_data)

    if not journey_objects:
        output.console.print("[red]No valid journeys to run[/red]", err=True)
        sys.exit(1)

    def runner_factory():
        client = Client(
            base_url=config.get("base_url", "http://localhost:8000"),
            timeout=config.get("timeout", 30),
        )
        return JourneyRunner(
            client=client,
            fail_fast=config.get("fail_fast", False),
            capture_logs=config.get("capture_logs", True),
            log_lines=config.get("log_lines", 50),
            ports=ports,
        )

    executor = ParallelJourneyExecutor(
        max_workers=parallel,
        fail_fast=config.get("fail_fast", False),
    )

    result = executor.run(journey_objects, runner_factory)

    # Convert to standard result format
    results = []
    for jr in result.journey_results:
        if jr:
            results.append({
                "journey_name": jr.journey_name,
                "success": jr.success,
                "started_at": jr.started_at.isoformat() if hasattr(jr, "started_at") else "",
                "finished_at": jr.finished_at.isoformat() if hasattr(jr, "finished_at") else "",
                "duration_ms": jr.duration_ms,
                "step_count": len(jr.step_results) if hasattr(jr, "step_results") else 0,
                "issues_count": len(jr.issues) if hasattr(jr, "issues") else 0,
                "issues": [
                    {
                        "step": i.step,
                        "path": i.path,
                        "error": i.error,
                        "severity": i.severity.value if hasattr(i.severity, "value") else str(i.severity),
                    }
                    for i in (jr.issues if hasattr(jr, "issues") else [])
                ],
            })

    ctx.obj["last_results"] = results

    if output_format == "json":
        import json

        click.echo(json.dumps(results, indent=2, default=str))
    else:
        output.console.print("\n[bold]Parallel Execution Results[/bold]")
        output.console.print(f"  Total: {result.total_journeys}")
        output.console.print(f"  Passed: [green]{result.passed}[/green]")
        output.console.print(f"  Failed: [red]{result.failed}[/red]")
        output.console.print(f"  Duration: {result.duration_ms:.2f}ms")
        output.console.print(f"  Success Rate: {result.success_rate:.1f}%")

        if result.errors:
            output.console.print("\n[bold red]Errors:[/bold red]")
            for error in result.errors:
                output.console.print(f"  [red]- {error}[/red]")

        _print_summary(results)

    sys.exit(EXIT_SUCCESS if result.failed == 0 else EXIT_FAILURE)


def _parse_ports(ports: tuple[str, ...], config: dict[str, Any]) -> dict[str, Any]:
    """Parse port configuration from CLI args and config."""
    result: dict[str, Any] = {}

    for port_config in config.get("ports", []):
        name = port_config.get("name")
        adapter_type = port_config.get("adapter_type")
        if name and adapter_type:
            result[name] = _create_port_adapter(name, adapter_type, port_config.get("config", {}))

    for port_str in ports:
        if "=" in port_str:
            name, adapter_type = port_str.split("=", 1)
            result[name] = _create_port_adapter(name, adapter_type, {})

    return result


def _create_port_adapter(name: str, adapter_type: str, config: dict[str, Any]) -> Any:
    """Create a port adapter instance."""
    from venomqa.adapters import get_adapter

    adapter_cls = get_adapter(adapter_type)
    if adapter_cls is None:
        raise ValueError(f"Unknown adapter type: {adapter_type}")
    return adapter_cls(**config)


def _load_journey(name: str, path: str) -> Any:
    """Load a journey from a Python file.

    Automatically adds the project root (parent of journeys/) to sys.path
    so that imports like 'from actions.sample_actions import ...' work
    without requiring sys.path hacks in the journey files.
    """
    import importlib.util
    import sys

    try:
        # Add project root to sys.path for action imports
        # The project root is the parent directory of the journey file's directory
        journey_dir = Path(path).parent.resolve()
        project_root = journey_dir.parent.resolve()

        # Add to sys.path if not already present
        project_root_str = str(project_root)
        if project_root_str not in sys.path:
            sys.path.insert(0, project_root_str)

        spec = importlib.util.spec_from_file_location(name, path)
        if spec is None or spec.loader is None:
            return None

        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

        if hasattr(module, "journey"):
            return module.journey
        elif hasattr(module, name):
            return getattr(module, name)

        for attr_name in dir(module):
            if not attr_name.startswith("_"):
                attr = getattr(module, attr_name)
                if hasattr(attr, "steps"):
                    return attr

        return None
    except Exception:
        logging.exception(f"Failed to load journey {name}")
        return None


def _count_steps(journey: Any) -> int:
    """Count total steps in a journey including branches."""
    count = 0
    steps = getattr(journey, "steps", [])

    for step in steps:
        from venomqa.core.models import Branch, Checkpoint, Step

        if isinstance(step, Step):
            count += 1
        elif isinstance(step, Branch):
            # Don't count branch paths in main progress
            pass
        elif isinstance(step, Checkpoint):
            # Don't count checkpoints as steps
            pass

    return count


def _execute_journey(
    journey: Any, config: dict[str, Any], no_infra: bool, ports: dict[str, Any]
) -> dict[str, Any]:
    """Execute a journey and return results."""
    from venomqa import Client
    from venomqa.cli.output import CLIOutput, ProgressConfig

    client = Client(base_url=config.get("base_url", "http://localhost:8000"), timeout=config.get("timeout", 30))

    # Create output handler
    output = CLIOutput(ProgressConfig())

    # Count total steps
    total_steps = _count_steps(journey)

    # Start journey with progress
    output.journey_start(
        name=getattr(journey, "name", "unknown"),
        description=getattr(journey, "description", ""),
        total_steps=total_steps,
    )

    # Get debug logger and step controller from config
    debug_logger = config.get("debug_logger")
    step_controller = config.get("step_controller")

    runner = JourneyRunner(
        client=client,
        fail_fast=config.get("fail_fast", False),
        capture_logs=config.get("capture_logs", True),
        log_lines=config.get("log_lines", 50),
        ports=ports,
        output=output,
        debug_logger=debug_logger,
        step_controller=step_controller,
    )

    try:
        result = runner.run(journey)

        # Count passed paths
        paths_passed = sum(1 for br in result.branch_results for pr in br.path_results if pr.success)
        paths_total = sum(len(br.path_results) for br in result.branch_results)

        # Show summary
        output.journey_summary(
            name=result.journey_name,
            success=result.success,
            step_count=len(result.step_results),
            passed_steps=sum(1 for s in result.step_results if s.success),
            duration_ms=result.duration_ms,
            issues=result.issues,
            paths_passed=paths_passed,
            paths_total=paths_total,
        )

        return {
            "journey_name": result.journey_name,
            "success": result.success,
            "started_at": result.started_at.isoformat(),
            "finished_at": result.finished_at.isoformat(),
            "duration_ms": result.duration_ms,
            "step_count": len(result.step_results),
            "issues_count": len(result.issues),
            "issues": [
                {
                    "step": i.step,
                    "path": i.path,
                    "error": i.error,
                    "severity": i.severity.value
                    if hasattr(i.severity, "value")
                    else str(i.severity),
                }
                for i in result.issues
            ],
        }
    except Exception as e:
        logging.exception("Journey execution failed")
        if output.live:
            output.live.stop()
        return {
            "journey_name": journey.name if hasattr(journey, "name") else "unknown",
            "success": False,
            "error": str(e),
        }


def _print_summary(results: list[dict[str, Any]]) -> None:
    """Print a summary of results."""
    from venomqa.cli.output import CLIOutput, ProgressConfig

    passed = sum(1 for r in results if r.get("success"))
    failed = len(results) - passed
    total_duration = sum(r.get("duration_ms", 0) for r in results)

    output = CLIOutput(ProgressConfig())
    output.overall_summary(
        total=len(results), passed=passed, failed=failed, total_duration_ms=total_duration
    )

    if failed > 0:
        output.console.print("\n[bold red]Failed journeys:[/bold red]")
        for r in results:
            if not r.get("success"):
                output.console.print(f"  [red]- {r.get('journey_name', 'unknown')}[/red]")
                for issue in r.get("issues", []):
                    output.console.print(
                        f"    [red]•[/red] {issue.get('step')}: {issue.get('error')}"
                    )


def _send_notifications(
    results: list[dict[str, Any]],
    config: dict[str, Any],
    notify_on_failure: bool,
    notify_always: bool,
    report_url: str | None,
    all_passed: bool,
    output: Any,
) -> None:
    """Send notifications based on test results.

    Args:
        results: List of journey result dictionaries.
        config: Configuration dictionary with notification settings.
        notify_on_failure: Whether to only notify on failures.
        notify_always: Whether to always notify (success or failure).
        report_url: Optional URL to include in notifications.
        all_passed: Whether all tests passed.
        output: CLI output handler.
    """
    from venomqa.notifications import (
        NotificationEvent,
        NotificationMessage,
        create_notification_manager_from_config,
    )

    # Skip if no notification is needed
    if notify_on_failure and all_passed:
        output.console.print("[dim]All tests passed, skipping failure notifications[/dim]")
        return

    # Get notification config from venomqa.yaml or create default
    notification_config = config.get("notifications", {})

    if not notification_config.get("channels"):
        # No channels configured - check for environment variables
        slack_webhook = config.get("SLACK_WEBHOOK") or None
        discord_webhook = config.get("DISCORD_WEBHOOK") or None

        if not slack_webhook and not discord_webhook:
            output.console.print(
                "[yellow]No notification channels configured. "
                "Add 'notifications' section to venomqa.yaml or set SLACK_WEBHOOK/DISCORD_WEBHOOK env vars.[/yellow]"
            )
            return

        # Build channel config from env vars
        channels = []
        if slack_webhook:
            channels.append({
                "type": "slack",
                "name": "slack",
                "webhook_url": slack_webhook,
                "on": ["failure", "recovery"] if notify_on_failure else ["failure", "recovery", "info"],
            })
        if discord_webhook:
            channels.append({
                "type": "discord",
                "name": "discord",
                "webhook_url": discord_webhook,
                "on": ["failure", "recovery"] if notify_on_failure else ["failure", "recovery", "info"],
            })
        notification_config["channels"] = channels

    # Create notification manager
    try:
        manager = create_notification_manager_from_config(
            {"notifications": notification_config},
            report_url=report_url,
        )
    except Exception as e:
        output.console.print(f"[yellow]Failed to initialize notifications: {e}[/yellow]")
        return

    # Build notification message
    passed = sum(1 for r in results if r.get("success"))
    failed = len(results) - passed
    total_duration = sum(r.get("duration_ms", 0) for r in results)

    if all_passed:
        event = NotificationEvent.INFO
        title = "All Tests Passed"
        body = f"{passed} journey(s) completed successfully in {total_duration/1000:.2f}s"
        severity = "info"
    else:
        event = NotificationEvent.FAILURE
        title = f"Test Failures: {failed} of {len(results)} journeys failed"
        # Collect failed journey info
        failed_journeys = [r for r in results if not r.get("success")]
        body_lines = [f"{failed} journey(s) failed out of {len(results)}"]
        for fj in failed_journeys[:3]:  # Show first 3 failures
            issues = fj.get("issues", [])
            if issues:
                body_lines.append(f"- {fj.get('journey_name')}: {issues[0].get('error', 'Unknown error')}")
            else:
                body_lines.append(f"- {fj.get('journey_name')}: Failed")
        if len(failed_journeys) > 3:
            body_lines.append(f"... and {len(failed_journeys) - 3} more")
        body = "\n".join(body_lines)
        severity = "high" if failed > 1 else "medium"

    message = NotificationMessage(
        title=title,
        body=body,
        event=event,
        severity=severity,
        report_url=report_url,
        metadata={
            "total_journeys": len(results),
            "passed_journeys": passed,
            "failed_journeys": failed,
            "total_duration_ms": total_duration,
        },
    )

    # Send notification
    output.console.print("[dim]Sending notifications...[/dim]")
    send_results = manager.send_message(message)

    for channel_name, success in send_results:
        if success:
            output.console.print(f"  [green]Sent to {channel_name}[/green]")
        else:
            output.console.print(f"  [red]Failed to send to {channel_name}[/red]")


@cli.command()
@click.option("--force", "-f", is_flag=True, help="Overwrite ALL files (including your actions/journeys)")
@click.option(
    "--path", "-p", "base_path", default="venomqa", help="Base path for QA directory (default: venomqa)"
)
@click.option(
    "--with-sample", "-s", is_flag=True, help="Include sample journey and actions"
)
@click.option(
    "--skip-checks", is_flag=True, help="Skip preflight checks"
)
@click.option(
    "--update", "-u", is_flag=True, help="Update framework files only (llm-context.md, README.md) — preserves your actions/journeys"
)
@click.pass_context
def init(ctx: click.Context, force: bool, base_path: str, with_sample: bool, skip_checks: bool, update: bool) -> None:
    """Initialize a new VenomQA project.

    \b
    Creates:
      venomqa/
      ├── venomqa.yaml        API URL and settings
      ├── llm-context.md      Paste into any AI assistant for help
      ├── actions/            Your action functions (api, context) -> response
      ├── fixtures/           Shared test data
      ├── journeys/           Exploration scripts using Agent.explore()
      └── reports/            Generated HTML/JSON reports

    \b
    File preservation:
      - YOUR FILES (never touched without --force): actions/, journeys/, fixtures/
      - FRAMEWORK FILES (safe to update): llm-context.md, README.md

    \b
    Examples:
      venomqa init                    Minimal scaffold (creates venomqa/)
      venomqa init --with-sample      Scaffold + working sample exploration
      venomqa init -p myproject       Use a different directory name
      venomqa init --update           Update framework files (preserves your code)
    """
    from rich.console import Console

    console = Console()

    # Run preflight checks first (unless skipped)
    if not skip_checks:
        console.print("\n[bold blue]Running preflight checks...[/bold blue]")
        try:
            from venomqa.cli.doctor import get_health_checks, run_health_checks

            checks = get_health_checks()
            # Only run essential checks
            essential_checks = [c for c in checks if c.required or c.name in ["Docker", "Docker Compose"]]
            passed, failed_required, failed_optional = run_health_checks(essential_checks, verbose=False)

            if failed_required > 0:
                console.print("\n[yellow]Some required dependencies are missing.[/yellow]")
                console.print("You can still initialize the project, but some features may not work.")
                if not click.confirm("Continue anyway?"):
                    sys.exit(1)
            console.print()
        except Exception as e:
            console.print(f"[yellow]Could not run preflight checks: {e}[/yellow]")

    base = Path(base_path)

    # Handle existing directory
    if base.exists():
        if update:
            # Update mode: only update framework files
            console.print(f"[bold]Updating VenomQA framework files in '{base}/'[/bold]\n")
        elif not force:
            console.print(f"[yellow]Directory '{base}' already exists.[/yellow]")
            console.print("Options:")
            console.print("  --update  Update framework files only (preserves your actions/journeys)")
            console.print("  --force   Overwrite ALL files (will delete your actions/journeys!)")
            sys.exit(1)
        else:
            console.print(f"[bold]Reinitializing VenomQA project in '{base}/'[/bold]\n")
    else:
        console.print(f"[bold]Initializing VenomQA project in '{base}/'[/bold]\n")

    dirs_to_create = [
        base,
        base / "actions",
        base / "fixtures",
        base / "journeys",
        base / "reports",
    ]

    # Generate llm-context.md content
    from venomqa.cli.llm_docs import LLM_CONTEXT

    # Separate framework files from user files
    # Framework files: safe to update anytime (generated, no user code)
    # User files: only touch if --force or file doesn't exist
    framework_files = [
        (base / "llm-context.md", LLM_CONTEXT),
        (base / "README.md", _get_readme_template(base_path)),
    ]

    user_files = [
        (base / "venomqa.yaml", VENVOMQA_YAML_TEMPLATE),
        (base / "docker-compose.qa.yml", DOCKER_COMPOSE_QA_TEMPLATE),
        (base / "actions" / "__init__.py", ACTIONS_INIT_PY),
        (base / "fixtures" / "__init__.py", FIXTURES_INIT_PY),
        (base / "journeys" / "__init__.py", JOURNEYS_INIT_PY),
    ]

    # Add sample files if requested (only if not in update mode)
    if with_sample and not update:
        user_files.extend([
            (base / "actions" / "sample_actions.py", SAMPLE_ACTION_PY),
            (base / "journeys" / "sample_journey.py", SAMPLE_JOURNEY_PY),
        ])

    # In update mode, only update framework files
    if update:
        files_to_create = framework_files
    else:
        files_to_create = framework_files + user_files

    # Create directories (skip in update mode if they exist)
    if not update:
        for dir_path in dirs_to_create:
            dir_path.mkdir(parents=True, exist_ok=True)
            if dir_path.exists():
                console.print(f"  [green]OK[/green] Created directory: {dir_path}")

    # Create/update files
    for file_path, content in files_to_create:
        is_framework_file = file_path in [f[0] for f in framework_files]

        if file_path.exists():
            if update and is_framework_file:
                # Always update framework files in update mode (with backup)
                backup_path = file_path.with_suffix(file_path.suffix + ".bak")
                import shutil
                shutil.copy(file_path, backup_path)
                file_path.write_text(content)
                console.print(f"  [green]OK[/green] Updated: {file_path} (backup: {backup_path.name})")
            elif force:
                # Force mode: overwrite everything
                file_path.write_text(content)
                console.print(f"  [yellow]++[/yellow] Overwrote: {file_path}")
            else:
                # Normal mode: skip existing files
                console.print(f"  [dim]--[/dim] Skipped (exists): {file_path}")
        else:
            # File doesn't exist: create it
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text(content)
            console.print(f"  [green]OK[/green] Created: {file_path}")

    # Create .gitignore for reports
    gitignore_path = base / "reports" / ".gitignore"
    gitignore_path.write_text("*\n!.gitignore\n")

    console.print(f"\n[bold green]VenomQA project initialized in '{base}/'[/bold green]")

    # Show setup options (skip in update mode)
    if not update:
        console.print("\n" + "=" * 70)
        console.print("[bold]State Exploration Setup[/bold]")
        console.print("=" * 70)
        console.print("""
VenomQA needs to distinguish states. Choose ONE:

[bold green]Option 1: state_from_context (easiest, no DB required)[/bold green]
  Track context keys to distinguish states. When values change,
  VenomQA sees a new state and explores downstream actions.

  world = World(
      api=HttpClient("http://localhost:8000"),
      state_from_context=["connection_id", "user_id", "item_count"],
  )

[bold yellow]Option 2: PostgreSQL (full DB rollback)[/bold yellow]
  VenomQA checkpoints and rolls back the database between branches.
  Must be the SAME database your API writes to.

  $ export DATABASE_URL="postgresql://user:pass@localhost:5432/yourdb"
  db = PostgresAdapter(os.environ["DATABASE_URL"])
  world = World(api=api, systems={"db": db})

[bold yellow]Option 3: SQLite (simpler DB rollback)[/bold yellow]
  world = World(api=api, systems={"db": SQLiteAdapter("/path/to/api.db")})
""")
        console.print("=" * 70)

    console.print("\n[bold]Next steps:[/bold]")
    console.print("  [bold red]1. Set up database rollback first![/bold red] (see above)")
    console.print(f"  2. Edit [cyan]{base}/venomqa.yaml[/cyan] — set base_url and db_url")
    console.print(f"  3. Write actions in [cyan]{base}/actions/[/cyan]")
    console.print(f"  4. Write invariants and run [cyan]Agent.explore()[/cyan]")

    if with_sample:
        console.print("\n[bold]Run the sample exploration:[/bold]")
        console.print(f"  python3 {base}/journeys/sample_journey.py")
    else:
        console.print("\n[bold]Generate sample files:[/bold]")
        console.print(f"  venomqa init --force --with-sample -p {base}")

    console.print(
        f"\n[bold cyan]AI assistant?[/bold cyan] Paste [cyan]{base}/llm-context.md[/cyan] "
        "into Claude, ChatGPT, or Cursor — it has all correct API signatures."
    )
    console.print("\n[bold]System check:[/bold]")
    console.print("  venomqa doctor")


@cli.command("list")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.pass_context
def list_journeys(ctx: click.Context, output_format: str) -> None:
    """List available journeys."""
    journeys = discover_journeys()

    if not journeys:
        click.echo("No journeys found. Create journeys in the 'journeys/' directory.")
        return

    if output_format == "json":
        import json

        click.echo(json.dumps(journeys, indent=2))
    else:
        click.echo(f"Found {len(journeys)} journey(s):\n")
        for name, info in journeys.items():
            click.echo(f"  • {name} ({info['path']})")


@cli.command()
@click.option("--config", "-c", "config_path", type=click.Path(), help="Path to config file")
@click.option("--journey", "-j", "journey_name", type=str, help="Validate specific journey")
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def validate(config_path: str | None, journey_name: str | None, output_json: bool) -> None:
    """Validate configuration and journey definitions.

    This command checks your VenomQA setup for common issues:
    - Configuration file syntax and values
    - Journey definition correctness
    - Action callable verification
    - Checkpoint naming conflicts

    \b
    Examples:
        venomqa validate                 # Validate all
        venomqa validate -c other.yaml   # Validate specific config
        venomqa validate -j checkout     # Validate specific journey
        venomqa validate --json          # Output as JSON

    Exit codes:
        0 - All validations passed
        1 - One or more validations failed
    """
    import json as json_module

    from rich.console import Console

    console = Console()
    issues: list[dict[str, Any]] = []
    warnings: list[dict[str, Any]] = []

    console.print("\n[bold blue]VenomQA Validation[/bold blue]")
    console.print("=" * 40)

    # Validate configuration
    console.print("\n[bold]Configuration[/bold]")
    try:
        config = load_config(config_path)
        console.print("  [green]OK[/green] Configuration file is valid")

        # Check for common issues
        if config.get("base_url", "").startswith("http://localhost"):
            warnings.append({
                "type": "config",
                "message": "base_url points to localhost - update for CI/CD",
                "suggestion": "Use environment variable: ${API_BASE_URL}",
            })
            console.print("  [yellow]--[/yellow] base_url points to localhost")

        if not config.get("db_url"):
            warnings.append({
                "type": "config",
                "message": "No db_url configured - state branching disabled",
                "suggestion": "Add db_url to enable checkpoints and rollbacks",
            })
            console.print("  [yellow]--[/yellow] No database URL configured (branching disabled)")

    except ConfigValidationError as e:
        issues.append({
            "type": "config",
            "message": str(e),
            "field": getattr(e, "field", None),
        })
        console.print(f"  [red]!![/red] {e}")
    except ConfigLoadError as e:
        issues.append({
            "type": "config",
            "message": str(e),
        })
        console.print(f"  [red]!![/red] {e}")

    # Validate journeys
    console.print("\n[bold]Journeys[/bold]")
    journeys = discover_journeys()

    if not journeys:
        warnings.append({
            "type": "journeys",
            "message": "No journeys found",
            "suggestion": "Create journeys in the 'journeys/' directory",
        })
        console.print("  [yellow]--[/yellow] No journeys found in journeys/ directory")
    else:
        journeys_to_check = {journey_name: journeys[journey_name]} if journey_name else journeys

        for name, info in journeys_to_check.items():
            try:
                journey_data = _load_journey(name, info["path"])
                if journey_data is None:
                    issues.append({
                        "type": "journey",
                        "journey": name,
                        "message": "Failed to load journey",
                    })
                    console.print(f"  [red]!![/red] {name}: Failed to load")
                else:
                    # Validate journey structure
                    if not hasattr(journey_data, "steps") or not journey_data.steps:
                        warnings.append({
                            "type": "journey",
                            "journey": name,
                            "message": "Journey has no steps",
                        })
                        console.print(f"  [yellow]--[/yellow] {name}: No steps defined")
                    else:
                        # Check step actions are callable
                        invalid_steps = []
                        for step in journey_data.steps:
                            if hasattr(step, "action"):
                                if not callable(step.action) and not isinstance(step.action, str):
                                    invalid_steps.append(step.name)

                        if invalid_steps:
                            issues.append({
                                "type": "journey",
                                "journey": name,
                                "message": f"Invalid step actions: {', '.join(invalid_steps)}",
                            })
                            console.print(f"  [red]!![/red] {name}: Invalid steps - {', '.join(invalid_steps)}")
                        else:
                            console.print(f"  [green]OK[/green] {name} ({len(journey_data.steps)} steps)")

            except Exception as e:
                issues.append({
                    "type": "journey",
                    "journey": name,
                    "message": str(e),
                })
                console.print(f"  [red]!![/red] {name}: {e}")

    # Summary
    console.print()
    if output_json:
        result = {
            "valid": len(issues) == 0,
            "issues": issues,
            "warnings": warnings,
        }
        click.echo(json_module.dumps(result, indent=2))
    else:
        if issues:
            console.print(f"[red]Validation failed with {len(issues)} error(s)[/red]")
            if warnings:
                console.print(f"[yellow]Plus {len(warnings)} warning(s)[/yellow]")
        elif warnings:
            console.print(f"[green]Validation passed[/green] with {len(warnings)} warning(s)")
        else:
            console.print("[green]All validations passed![/green]")

    sys.exit(1 if issues else 0)


@cli.command()
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["markdown", "json", "junit", "html"]),
    default="markdown",
    help="Report format",
)
@click.option(
    "--output", "-o", "output_path", type=click.Path(), default=None, help="Output file path"
)
@click.pass_context
def report(ctx: click.Context, output_format: str, output_path: str | None) -> None:
    """Generate report from last run."""
    results = ctx.obj.get("last_results")

    if not results:
        click.echo("No results available. Run a journey first with 'venomqa run'.", err=True)
        sys.exit(1)

    config: dict[str, Any] = ctx.obj["config"]
    report_dir = Path(config.get("report_dir", "reports"))
    report_dir.mkdir(parents=True, exist_ok=True)

    if output_path is None:
        ext_map = {"markdown": "md", "json": "json", "junit": "xml", "html": "html"}
        output_path = str(report_dir / f"report.{ext_map.get(output_format, 'txt')}")

    report_content = _generate_report(results, output_format)

    with open(output_path, "w") as f:
        f.write(report_content)

    click.echo(f"Report generated: {output_path}")


def _generate_report(results: list[dict[str, Any]], output_format: str) -> str:
    """Generate report in specified format."""
    if output_format == "json":
        import json

        return json.dumps(results, indent=2, default=str)

    if output_format == "junit":
        return _generate_junit_report(results)

    if output_format == "html":
        return _generate_html_report(results)

    return _generate_markdown_report(results)


def _generate_markdown_report(results: list[dict[str, Any]]) -> str:
    """Generate markdown report."""
    lines = ["# VenomQA Test Report\n"]
    lines.append(f"**Total Journeys**: {len(results)}")
    passed = sum(1 for r in results if r.get("success"))
    lines.append(f"**Passed**: {passed}")
    lines.append(f"**Failed**: {len(results) - passed}\n")

    for result in results:
        status = "✓" if result.get("success") else "✗"
        lines.append(f"## {status} {result.get('journey_name', 'unknown')}\n")
        lines.append(f"- **Duration**: {result.get('duration_ms', 0):.0f}ms")
        lines.append(f"- **Steps**: {result.get('step_count', 0)}")

        issues = result.get("issues", [])
        if issues:
            lines.append(f"- **Issues**: {len(issues)}\n")
            for issue in issues:
                lines.append(f"  - `{issue.get('step')}`: {issue.get('error')}")
        lines.append("")

    return "\n".join(lines)


def _generate_junit_report(results: list[dict[str, Any]]) -> str:
    """Generate JUnit XML report."""
    import xml.etree.ElementTree as ET

    testsuites = ET.Element("testsuites")
    testsuite = ET.SubElement(testsuites, "testsuite")
    testsuite.set("name", "VenomQA")
    testsuite.set("tests", str(len(results)))

    failures = sum(1 for r in results if not r.get("success"))
    testsuite.set("failures", str(failures))

    for result in results:
        testcase = ET.SubElement(testsuite, "testcase")
        testcase.set("name", result.get("journey_name", "unknown"))
        testcase.set("time", str(result.get("duration_ms", 0) / 1000))

        if not result.get("success"):
            failure = ET.SubElement(testcase, "failure")
            issues = result.get("issues", [])
            if issues:
                failure.set("message", issues[0].get("error", "Unknown error"))
                failure.text = "\n".join(f"{i.get('step')}: {i.get('error')}" for i in issues)

    return ET.tostring(testsuites, encoding="unicode")


def _generate_html_report(results: list[dict[str, Any]]) -> str:
    """Generate HTML report."""
    passed = sum(1 for r in results if r.get("success"))
    failed = len(results) - passed

    html = f"""<!DOCTYPE html>
<html>
<head>
    <title>VenomQA Report</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                margin: 40px; }}
        .summary {{ background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
        .passed {{ color: #22c55e; }}
        .failed {{ color: #ef4444; }}
        .journey {{ border: 1px solid #e5e5e5; padding: 16px; margin-bottom: 12px;
                    border-radius: 8px; }}
        .issue {{ background: #fef2f2; padding: 8px; margin-top: 8px; border-radius: 4px; }}
    </style>
</head>
<body>
    <h1>VenomQA Report</h1>
    <div class="summary">
        <strong>Total:</strong> {len(results)} |
        <span class="passed">Passed: {passed}</span> |
        <span class="failed">Failed: {failed}</span>
    </div>
"""

    for result in results:
        status_class = "passed" if result.get("success") else "failed"
        status_icon = "✓" if result.get("success") else "✗"
        html += f"""
    <div class="journey">
        <h3><span class="{status_class}">{status_icon}</span> \
{result.get("journey_name", "unknown")}</h3>
        <p>Duration: {result.get("duration_ms", 0):.0f}ms | Steps: {result.get("step_count", 0)}</p>
"""
        for issue in result.get("issues", []):
            html += f"""
        <div class="issue">
            <strong>{issue.get("step")}</strong>: {issue.get("error")}
        </div>
"""
        html += "    </div>"

    html += """
</body>
</html>"""
    return html


@cli.command("load")
@click.argument("journey_name")
@click.option("--users", "-u", default=10, type=int, help="Number of concurrent users (default: 10)")
@click.option("--duration", "-d", default="60s", help="Test duration (e.g., 60s, 2m, 1h) (default: 60s)")
@click.option("--ramp-up", "-r", default="0s", help="Ramp-up time (e.g., 10s, 1m) (default: 0s)")
@click.option("--think-time", "-t", default="0s", help="Think time between requests (e.g., 1s, 1-3s)")
@click.option("--warmup", "-w", default="0s", help="Warmup period before collecting metrics")
@click.option(
    "--pattern",
    type=click.Choice(["constant", "ramp_up", "spike", "stress"]),
    default="constant",
    help="Load pattern (default: constant)",
)
@click.option("--output", "-o", "output_path", type=click.Path(), help="Output file for report")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["json", "html", "markdown", "text"]),
    default="text",
    help="Output format (default: text)",
)
@click.option("--p99", type=float, help="Assert p99 latency is below this value (ms)")
@click.option("--error-rate", type=float, help="Assert error rate is below this value (%)")
@click.option("--throughput", type=float, help="Assert throughput is above this value (req/s)")
@click.pass_context
def load_test(
    ctx: click.Context,
    journey_name: str,
    users: int,
    duration: str,
    ramp_up: str,
    think_time: str,
    warmup: str,
    pattern: str,
    output_path: str | None,
    output_format: str,
    p99: float | None,
    error_rate: float | None,
    throughput: float | None,
) -> None:
    """Run load tests against a journey.

    Execute a journey with multiple concurrent users to measure performance
    under load. Collects metrics including response times (p50, p90, p95, p99),
    throughput (requests/second), and error rates.

    \b
    Examples:
        venomqa load checkout_journey --users 100 --duration 60s
        venomqa load payment_flow -u 50 -d 2m --ramp-up 10s
        venomqa load api_test -u 100 -d 60s --think-time 1-3s
        venomqa load checkout -u 100 -d 60s --p99 500 --error-rate 1
        venomqa load checkout -u 100 -d 60s -f html -o report.html

    \b
    Load patterns:
        constant  - Maintain constant load throughout the test
        ramp_up   - Gradually increase load from 0 to target
        spike     - Sudden increase followed by sustained load
        stress    - Increase load until system breaks

    \b
    Assertions (fail if not met):
        --p99 500       - P99 latency must be below 500ms
        --error-rate 1  - Error rate must be below 1%
        --throughput 100 - Throughput must be above 100 req/s
    """
    from venomqa.cli.output import CLIOutput, ProgressConfig
    from venomqa.performance.load_tester import (
        LoadTestAssertions,
        LoadTestConfig,
        LoadTester,
    )

    config: dict[str, Any] = ctx.obj.get("config", {})
    output = CLIOutput(ProgressConfig())

    # Discover and load the journey
    journeys = discover_journeys()
    if journey_name not in journeys:
        click.echo(f"Journey not found: {journey_name}", err=True)
        click.echo(f"Available journeys: {', '.join(journeys.keys())}", err=True)
        sys.exit(1)

    journey = _load_journey(journey_name, journeys[journey_name]["path"])
    if journey is None:
        click.echo(f"Failed to load journey: {journey_name}", err=True)
        sys.exit(1)

    # Create load test config
    load_config_dict = {
        "users": users,
        "duration": duration,
        "ramp_up": ramp_up,
        "think_time": think_time,
        "warmup": warmup,
        "pattern": pattern,
    }
    load_config = LoadTestConfig.from_dict(load_config_dict)

    # Display test configuration
    output.console.print("\n[bold cyan]Load Test Configuration[/bold cyan]")
    output.console.print(f"  Journey: {journey_name}")
    output.console.print(f"  Users: {load_config.concurrent_users}")
    output.console.print(f"  Duration: {load_config.duration_seconds}s")
    output.console.print(f"  Ramp-up: {load_config.ramp_up_seconds}s")
    output.console.print(f"  Think time: {load_config.think_time_min}-{load_config.think_time_max}s")
    output.console.print(f"  Pattern: {load_config.pattern.value}")
    if load_config.warmup_seconds > 0:
        output.console.print(f"  Warmup: {load_config.warmup_seconds}s")
    output.console.print()

    # Create runner factory
    from venomqa import Client

    def runner_factory() -> JourneyRunner:
        client = Client(
            base_url=config.get("base_url", "http://localhost:8000"),
            timeout=config.get("timeout", 30),
        )
        return JourneyRunner(
            client=client,
            fail_fast=False,
            capture_logs=False,
        )

    # Progress callback
    last_rps = [0.0]

    def progress_callback(metrics) -> None:
        snapshot = metrics.get_snapshot()
        rps = snapshot["actual_rps"]
        if abs(rps - last_rps[0]) > 0.5:
            output.console.print(
                f"  [dim]Users: {snapshot['active_users']}, "
                f"Requests: {snapshot['total_requests']}, "
                f"RPS: {rps:.1f}, "
                f"Errors: {snapshot['failed_requests']}[/dim]"
            )
            last_rps[0] = rps

    # Run load test
    output.console.print("[bold]Starting load test...[/bold]")
    tester = LoadTester(load_config, progress_callback=progress_callback)

    try:
        result = tester.run(journey, runner_factory)
    except KeyboardInterrupt:
        output.console.print("\n[yellow]Load test interrupted by user[/yellow]")
        tester.stop()
        sys.exit(1)

    # Display results
    if output_format == "text":
        output.console.print(f"\n[bold green]{result.get_summary()}[/bold green]")
    elif output_format == "json":
        import json

        click.echo(json.dumps(result.to_dict(), indent=2, default=str))
    elif output_format in ("html", "markdown"):
        # Save to file
        if output_path:
            result.save_report(output_path, format=output_format)
            output.console.print(f"\n[green]Report saved to: {output_path}[/green]")
        else:
            # Generate and print
            if output_format == "markdown":
                click.echo(result._generate_markdown_report())
            else:
                click.echo(result._generate_html_report())

    # Save report if output path specified
    if output_path and output_format not in ("html", "markdown"):
        result.save_report(output_path, format="json" if output_format == "json" else "json")
        output.console.print(f"\n[green]Report saved to: {output_path}[/green]")

    # Validate assertions
    if p99 is not None or error_rate is not None or throughput is not None:
        assertions = LoadTestAssertions(
            max_p99_ms=p99,
            max_error_rate_percent=error_rate,
            min_throughput_rps=throughput,
        )
        passed, failures = assertions.validate(result)

        if not passed:
            output.console.print("\n[bold red]Load test assertions FAILED:[/bold red]")
            for failure in failures:
                output.console.print(f"  [red]- {failure}[/red]")
            sys.exit(1)
        else:
            output.console.print("\n[bold green]All load test assertions PASSED[/bold green]")

    # Exit with error if error rate is too high (> 50%)
    if result.error_rate > 50:
        sys.exit(1)


@cli.command()
@click.argument("journey_names", nargs=-1)
@click.option("--all", "-a", "run_all", is_flag=True, help="Watch and run all journeys")
@click.option(
    "--path",
    "-p",
    "base_path",
    default=".",
    type=click.Path(exists=True),
    help="Base path containing actions/, fixtures/, journeys/ directories",
)
@click.pass_context
def watch(
    ctx: click.Context,
    journey_names: tuple[str, ...],
    run_all: bool,
    base_path: str,
) -> None:
    """Watch for file changes and automatically re-run affected journeys.

    This command monitors the actions/, fixtures/, and journeys/ directories
    for changes and intelligently re-runs only the affected journeys.

    \b
    Examples:
        venomqa watch                    # Watch all journeys
        venomqa watch --all              # Same as above
        venomqa watch user_login         # Watch specific journey
        venomqa watch checkout payment   # Watch multiple journeys
        venomqa watch -p ./qa            # Watch from specific path

    \b
    Smart re-running:
        - If an action file changes, re-run journeys using that action
        - If a fixture file changes, re-run journeys using that fixture
        - If a journey file changes, re-run that specific journey

    Press Ctrl+C to stop watching.
    """
    try:
        from venomqa.cli.watch import run_watch_mode
    except ImportError as e:
        click.echo(
            "Watch mode requires the 'watchdog' package. "
            "Install it with: pip install watchdog",
            err=True,
        )
        click.echo(f"Import error: {e}", err=True)
        sys.exit(1)

    config: dict[str, Any] = ctx.obj.get("config", {})
    journey_list = list(journey_names) if journey_names else None

    # If no journeys specified and --all not set, default to --all
    if not journey_list and not run_all:
        run_all = True

    run_watch_mode(
        base_path=Path(base_path),
        journey_names=journey_list,
        run_all=run_all,
        config=config,
    )


@cli.command()
@click.argument("journey_names", nargs=-1, required=True)
@click.option("--iterations", "-i", type=int, default=100, help="Iterations (default: 100)")
@click.option("--warmup", "-w", type=int, default=10, help="Warmup iterations (default: 10)")
@click.option(
    "--format", "-f", "output_format", type=click.Choice(["text", "json", "csv"]),
    default="text", help="Output format",
)
@click.option("--output", "-o", "output_path", type=click.Path(), help="Output file path")
@click.option("--track-memory", is_flag=True, help="Track memory usage")
@click.option("--port", "ports", multiple=True, help="Port config: name=adapter_type")
@click.pass_context
def benchmark(
    ctx: click.Context,
    journey_names: tuple[str, ...],
    iterations: int,
    warmup: int,
    output_format: str,
    output_path: str | None,
    track_memory: bool,
    ports: tuple[str, ...],
) -> None:
    """Benchmark journeys for performance analysis."""
    from venomqa import Client
    from venomqa.cli.output import CLIOutput, ProgressConfig
    from venomqa.performance.benchmark import BenchmarkConfig, BenchmarkResult, BenchmarkSuite

    config: dict[str, Any] = ctx.obj["config"]
    parsed_ports = _parse_ports(ports, config)
    journeys = discover_journeys()

    if not journeys:
        click.echo("No journeys found.", err=True)
        sys.exit(1)

    for name in journey_names:
        if name not in journeys:
            click.echo(f"Journey not found: {name}", err=True)
            sys.exit(1)

    output = CLIOutput(ProgressConfig())
    output.console.print(f"\n[bold]Benchmarking {len(journey_names)} journey(s)...[/bold]\n")

    bench_config = BenchmarkConfig(
        iterations=iterations, warmup_iterations=warmup,
        track_memory=track_memory, verbose=ctx.obj.get("verbose", False),
    )
    suite = BenchmarkSuite(bench_config)

    for journey_name in journey_names:
        journey_data = _load_journey(journey_name, journeys[journey_name]["path"])
        if not journey_data:
            continue

        def make_factory(j=journey_data):
            def factory():
                client = Client(
                    base_url=config.get("base_url", "http://localhost:8000"),
                    timeout=config.get("timeout", 30),
                )
                return JourneyRunner(client=client, fail_fast=False, capture_logs=False, ports=parsed_ports)
            return factory
        suite.add(journey_data, make_factory())

    results = suite.run_all()

    if output_format == "json":
        import json
        data = [r.to_dict() for r in results]
        if output_path:
            with open(output_path, "w") as f:
                json.dump(data, f, indent=2)
        else:
            click.echo(json.dumps(data, indent=2))
    elif output_format == "csv":
        lines = [BenchmarkResult.csv_header()] + [r.to_csv_row() for r in results]
        csv_content = "\n".join(lines)
        if output_path:
            with open(output_path, "w") as f:
                f.write(csv_content)
        else:
            click.echo(csv_content)
    else:
        for result in results:
            output.console.print(result.get_summary())
        if len(results) > 1:
            comparison = suite.get_comparison()
            output.console.print(f"[bold]Fastest:[/bold] {comparison.get('fastest')}")
        if output_path:
            suite.export_json(output_path)

    sys.exit(0)


@cli.command("generate")
@click.option(
    "--from-openapi",
    "openapi_path",
    type=click.Path(exists=True),
    required=True,
    help="Path to OpenAPI specification (YAML or JSON)",
)
@click.option(
    "--output",
    "-o",
    "output_dir",
    type=click.Path(),
    default="qa/actions",
    help="Output directory for generated files (default: qa/actions)",
)
@click.option(
    "--actions-file",
    default="actions.py",
    help="Filename for actions module (default: actions.py)",
)
@click.option(
    "--fixtures-file",
    default="fixtures.py",
    help="Filename for fixtures module (default: fixtures.py)",
)
@click.option(
    "--no-fixtures",
    is_flag=True,
    help="Skip generating fixture factories",
)
@click.option(
    "--group-by-tags",
    is_flag=True,
    help="Group actions by OpenAPI tags into separate files",
)
@click.option(
    "--no-docstrings",
    is_flag=True,
    help="Skip generating docstrings",
)
@click.option(
    "--no-type-hints",
    is_flag=True,
    help="Skip generating type hints",
)
@click.pass_context
def generate(
    ctx: click.Context,
    openapi_path: str,
    output_dir: str,
    actions_file: str,
    fixtures_file: str,
    no_fixtures: bool,
    group_by_tags: bool,
    no_docstrings: bool,
    no_type_hints: bool,
) -> None:
    """Generate VenomQA actions and fixtures from an OpenAPI specification.

    Parses an OpenAPI 3.0 specification and generates:
    - Action functions for each endpoint
    - Fixture factories for schema definitions

    Examples:
        venomqa generate --from-openapi openapi.yaml
        venomqa generate --from-openapi api.json --output qa/generated/
        venomqa generate --from-openapi spec.yaml --group-by-tags
    """
    from venomqa.generators import GeneratorConfig, OpenAPIGenerator, OpenAPIParseError

    config = GeneratorConfig(
        output_dir=output_dir,
        actions_file=actions_file,
        fixtures_file=fixtures_file,
        generate_fixtures=not no_fixtures,
        group_by_tags=group_by_tags,
        include_docstrings=not no_docstrings,
        include_type_hints=not no_type_hints,
    )

    click.echo(f"Loading OpenAPI specification from: {openapi_path}")

    try:
        generator = OpenAPIGenerator(openapi_path, config=config)
        schema = generator.load()

        click.echo(f"  API: {schema.title} v{schema.version}")
        click.echo(f"  Endpoints: {len(schema.endpoints)}")
        click.echo(f"  Schemas: {len(schema.schemas)}")

        click.echo(f"\nGenerating code to: {output_dir}/")
        generated_files = generator.generate(output_dir)

        click.echo("\nGenerated files:")
        for filename in generated_files:
            click.echo(f"  - {output_dir}/{filename}")

        click.echo(f"\nSuccessfully generated {len(generated_files)} file(s)!")

    except OpenAPIParseError as e:
        click.echo(f"Error parsing OpenAPI specification: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error generating code: {e}", err=True)
        if ctx.obj.get("verbose"):
            import traceback

            traceback.print_exc()
        sys.exit(1)


@cli.command("generate-graphql")
@click.option(
    "--from-schema",
    "schema_path",
    type=click.Path(exists=True),
    help="Path to GraphQL schema file (SDL or JSON introspection)",
)
@click.option(
    "--from-endpoint",
    "endpoint_url",
    help="GraphQL endpoint URL for introspection",
)
@click.option(
    "--output",
    "-o",
    "output_dir",
    type=click.Path(),
    default="qa/actions",
    help="Output directory for generated files (default: qa/actions)",
)
@click.option(
    "--module-name",
    default="graphql_actions",
    help="Module name for generated file (default: graphql_actions)",
)
@click.option(
    "--no-docstrings",
    is_flag=True,
    help="Skip generating docstrings",
)
@click.option(
    "--no-type-hints",
    is_flag=True,
    help="Skip generating type hints",
)
@click.option(
    "--header",
    "-H",
    "headers",
    multiple=True,
    help="HTTP headers for introspection (format: 'Name: Value')",
)
@click.pass_context
def generate_graphql(
    ctx: click.Context,
    schema_path: str | None,
    endpoint_url: str | None,
    output_dir: str,
    module_name: str,
    no_docstrings: bool,
    no_type_hints: bool,
    headers: tuple[str, ...],
) -> None:
    """Generate VenomQA actions from a GraphQL schema.

    Parses a GraphQL schema (from file or introspection endpoint) and generates
    action functions for queries, mutations, and subscriptions.

    \b
    Examples:
        venomqa generate-graphql --from-schema schema.graphql
        venomqa generate-graphql --from-schema introspection.json
        venomqa generate-graphql --from-endpoint https://api.example.com/graphql
        venomqa generate-graphql --from-endpoint http://localhost:4000/graphql -H "Authorization: Bearer token"

    \b
    Generated code includes:
        - Query functions decorated with @query
        - Mutation functions decorated with @mutation
        - Subscription generators decorated with @subscription
        - Full type hints and docstrings
    """
    if schema_path is None and endpoint_url is None:
        click.echo(
            "Error: Either --from-schema or --from-endpoint must be provided",
            err=True,
        )
        sys.exit(1)

    if schema_path is not None and endpoint_url is not None:
        click.echo(
            "Error: Cannot use both --from-schema and --from-endpoint",
            err=True,
        )
        sys.exit(1)

    try:
        from venomqa.graphql.codegen import generate_actions_from_schema

        schema_source: str | Path | dict[str, Any]

        if schema_path:
            click.echo(f"Loading GraphQL schema from: {schema_path}")
            schema_source = Path(schema_path)
        else:
            # Introspect from endpoint
            click.echo(f"Introspecting GraphQL schema from: {endpoint_url}")

            # Parse headers
            http_headers: dict[str, str] = {}
            for header in headers:
                if ":" in header:
                    name, value = header.split(":", 1)
                    http_headers[name.strip()] = value.strip()

            # Use GraphQL client to introspect
            from venomqa.http.graphql import INTROSPECTION_QUERY, GraphQLClient

            client = GraphQLClient(
                endpoint=endpoint_url,
                default_headers=http_headers,
            )
            client.connect()

            try:
                response = client.execute(INTROSPECTION_QUERY)
                if response.has_errors:
                    click.echo(f"Introspection failed: {response.errors}", err=True)
                    sys.exit(1)

                schema_source = {"data": {"__schema": response.get_data("__schema")}}
            finally:
                client.disconnect()

        # Generate code
        click.echo(f"\nGenerating actions to: {output_dir}/")

        code = generate_actions_from_schema(
            schema_source=schema_source,
            output_dir=output_dir,
            module_name=module_name,
            include_docstrings=not no_docstrings,
            include_type_hints=not no_type_hints,
        )

        # Count generated items
        query_count = code.count("@query(")
        mutation_count = code.count("@mutation(")
        subscription_count = code.count("@subscription(")

        click.echo("\nGenerated files:")
        click.echo(f"  - {output_dir}/{module_name}.py")
        click.echo(f"  - {output_dir}/__init__.py")

        click.echo("\nGenerated actions:")
        click.echo(f"  - Queries: {query_count}")
        click.echo(f"  - Mutations: {mutation_count}")
        click.echo(f"  - Subscriptions: {subscription_count}")

        click.echo("\nSuccessfully generated GraphQL actions!")
        click.echo("\nUsage example:")
        click.echo(f"    from {output_dir.replace('/', '.')}.{module_name} import *")

    except FileNotFoundError as e:
        click.echo(f"Schema file not found: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error generating code: {e}", err=True)
        if ctx.obj.get("verbose"):
            import traceback

            traceback.print_exc()
        sys.exit(1)


# Environment management commands
@cli.command("check-env")
@click.argument("environment_name")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.pass_context
def check_env(ctx: click.Context, environment_name: str, output_format: str) -> None:
    """Check health of an environment.

    Verifies connectivity, permissions, and database access for the specified
    environment.

    Examples:
        venomqa check-env staging
        venomqa check-env production --format json
    """
    from venomqa.environments import EnvironmentManager

    try:
        env_manager = EnvironmentManager("venomqa.yaml")
    except Exception as e:
        click.echo(f"Error loading environments: {e}", err=True)
        sys.exit(EXIT_CONFIG_ERROR)

    if environment_name not in env_manager.list_environments():
        click.echo(f"Environment not found: {environment_name}", err=True)
        click.echo("Available environments: " + ", ".join(env_manager.list_environments()))
        sys.exit(EXIT_CONFIG_ERROR)

    click.echo(f"Checking environment: {environment_name}...")

    health = env_manager.check_health(environment_name)

    if output_format == "json":
        import json

        click.echo(json.dumps(health.to_dict(), indent=2))
    else:
        status = "HEALTHY" if health.overall_healthy else "UNHEALTHY"
        status_color = "green" if health.overall_healthy else "red"

        from venomqa.cli.output import CLIOutput, ProgressConfig

        output = CLIOutput(ProgressConfig())
        output.console.print(f"\n[bold]Environment: {environment_name}[/bold]")
        output.console.print(f"Status: [{status_color}]{status}[/{status_color}]")
        output.console.print("\nHealth Checks:")

        for check in health.checks:
            check_status = "PASS" if check.healthy else "FAIL"
            check_color = "green" if check.healthy else "red"
            output.console.print(
                f"  [{check_color}]{check_status}[/{check_color}] {check.name}: {check.message} ({check.duration_ms:.0f}ms)"
            )

    sys.exit(EXIT_SUCCESS if health.overall_healthy else EXIT_FAILURE)


@cli.command("list-envs")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.pass_context
def list_envs(ctx: click.Context, output_format: str) -> None:
    """List all configured environments.

    Examples:
        venomqa list-envs
        venomqa list-envs --format json
    """
    from venomqa.environments import EnvironmentManager

    try:
        env_manager = EnvironmentManager("venomqa.yaml")
    except Exception as e:
        click.echo(f"Error loading environments: {e}", err=True)
        sys.exit(EXIT_CONFIG_ERROR)

    environments = env_manager.get_all_environments()

    if not environments:
        click.echo("No environments configured.")
        click.echo("Add environments to your venomqa.yaml file.")
        return

    if output_format == "json":
        import json

        data = {name: env.config.to_dict() for name, env in environments.items()}
        click.echo(json.dumps(data, indent=2))
    else:
        from venomqa.cli.output import CLIOutput, ProgressConfig

        output = CLIOutput(ProgressConfig())
        output.console.print(f"\n[bold]Configured Environments ({len(environments)}):[/bold]\n")

        default_env = env_manager.default_environment

        for name, env in environments.items():
            is_default = " (default)" if name == default_env else ""
            mode = env.config.mode.value
            mode_indicator = "[dim][read-only][/dim]" if env.is_read_only else ""

            output.console.print(f"  [bold]{name}[/bold]{is_default} {mode_indicator}")
            output.console.print(f"    URL: {env.base_url}")
            output.console.print(f"    Mode: {mode}")
            if env.database:
                # Mask password in database URL
                db_url = env.database
                if "@" in db_url:
                    parts = db_url.split("@")
                    db_url = parts[0].split(":")[0] + ":****@" + parts[1]
                output.console.print(f"    Database: {db_url}")
            output.console.print("")


@cli.command("compare-envs")
@click.argument("journey_name")
@click.argument("env1")
@click.argument("env2")
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.option(
    "--ignore",
    "-i",
    "ignore_fields",
    multiple=True,
    help="Fields to ignore in comparison (can be specified multiple times)",
)
@click.pass_context
def compare_envs(
    ctx: click.Context,
    journey_name: str,
    env1: str,
    env2: str,
    output_format: str,
    ignore_fields: tuple[str, ...],
) -> None:
    """Compare journey results between two environments.

    Runs the specified journey in both environments and highlights differences
    in responses.

    Examples:
        venomqa compare-envs user_login staging production
        venomqa compare-envs checkout staging production --ignore timestamp --ignore id
    """
    from venomqa.environments import EnvironmentManager

    try:
        env_manager = EnvironmentManager("venomqa.yaml")
    except Exception as e:
        click.echo(f"Error loading environments: {e}", err=True)
        sys.exit(EXIT_CONFIG_ERROR)

    # Verify environments exist
    for env_name in [env1, env2]:
        if env_name not in env_manager.list_environments():
            click.echo(f"Environment not found: {env_name}", err=True)
            click.echo("Available environments: " + ", ".join(env_manager.list_environments()))
            sys.exit(EXIT_CONFIG_ERROR)

    # Load journey
    journeys = discover_journeys()
    if journey_name not in journeys:
        click.echo(f"Journey not found: {journey_name}", err=True)
        sys.exit(EXIT_FAILURE)

    journey = _load_journey(journey_name, journeys[journey_name]["path"])
    if journey is None:
        click.echo(f"Failed to load journey: {journey_name}", err=True)
        sys.exit(EXIT_FAILURE)

    from venomqa.cli.output import CLIOutput, ProgressConfig

    output = CLIOutput(ProgressConfig())
    output.console.print(f"\n[bold]Comparing {journey_name} across environments...[/bold]")
    output.console.print(f"  Environment 1: {env1}")
    output.console.print(f"  Environment 2: {env2}")

    # Run comparison
    ignore_list = list(ignore_fields) if ignore_fields else None
    comparison = env_manager.compare_environments(
        journey=journey,
        env1_name=env1,
        env2_name=env2,
        ignore_fields=ignore_list,
    )

    if output_format == "json":
        import json

        click.echo(json.dumps(comparison.to_dict(), indent=2, default=str))
    else:
        output.console.print("\n[bold]Results:[/bold]")
        output.console.print(f"  Both passed: {'Yes' if comparison.both_passed else 'No'}")
        output.console.print(f"  Differences found: {len(comparison.differences)}")

        if comparison.differences:
            output.console.print("\n[bold]Differences:[/bold]")
            for diff in comparison.differences:
                output.console.print(f"\n  [yellow]{diff.path}[/yellow]")
                output.console.print(f"    Type: {diff.difference_type}")
                output.console.print(f"    {env1}: {diff.env1_value}")
                output.console.print(f"    {env2}: {diff.env2_value}")

    sys.exit(EXIT_SUCCESS if comparison.both_passed and not comparison.has_differences else EXIT_FAILURE)



@cli.command()
@click.argument("seed_file", type=click.Path(exists=True))
@click.option(
    "--mode",
    "-m",
    "seed_mode",
    type=click.Choice(["api", "database", "hybrid"]),
    default="api",
    help="Seeding mode (api, database, or hybrid)",
)
@click.option(
    "--prefix",
    "-p",
    "prefix",
    default=None,
    help="Unique prefix for test isolation (auto-generated if not provided)",
)
@click.option(
    "--endpoint",
    "-e",
    "endpoints",
    multiple=True,
    help="API endpoint mapping (format: resource_type=/api/path)",
)
@click.option(
    "--table",
    "-t",
    "tables",
    multiple=True,
    help="Table mapping (format: resource_type=table_name)",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Show what would be seeded without actually seeding",
)
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.pass_context
def seed(
    ctx: click.Context,
    seed_file: str,
    seed_mode: str,
    prefix: str | None,
    endpoints: tuple[str, ...],
    tables: tuple[str, ...],
    dry_run: bool,
    output_format: str,
) -> None:
    """Seed test data from a YAML or JSON file.

    Loads seed data from the specified file and creates resources either
    via API calls or direct database inserts.

    \b
    Examples:
        venomqa seed seeds/base.yaml
        venomqa seed seeds/users.yaml --mode database
        venomqa seed seeds/ecommerce.yaml --prefix test_run_001
        venomqa seed seeds/api.yaml -e users=/api/v1/users -e products=/api/v1/products

    \b
    Seed file format (YAML)::

        users:
          - id: user_1
            email: test@example.com
        products:
          - id: product_1
            seller_id: "{{users.user_1.id}}"
    """
    from venomqa.data import SeedConfig, SeedManager, SeedMode

    # Parse endpoint mappings
    api_endpoints: dict[str, str] = {}
    for endpoint in endpoints:
        if "=" in endpoint:
            resource_type, path = endpoint.split("=", 1)
            api_endpoints[resource_type] = path

    # Parse table mappings
    table_mapping: dict[str, str] = {}
    for table in tables:
        if "=" in table:
            resource_type, table_name = table.split("=", 1)
            table_mapping[resource_type] = table_name

    # Map mode string to enum
    mode_map = {
        "api": SeedMode.API,
        "database": SeedMode.DATABASE,
        "hybrid": SeedMode.HYBRID,
    }
    mode = mode_map[seed_mode]

    config = SeedConfig(
        mode=mode,
        prefix=prefix or "",
        api_endpoints=api_endpoints,
        table_mapping=table_mapping,
    )

    # Get client and database from config
    config_obj: dict[str, Any] = ctx.obj.get("config", {})
    client = None
    database = None

    # Create client if API mode
    if mode in (SeedMode.API, SeedMode.HYBRID):
        from venomqa import Client

        client = Client(
            base_url=config_obj.get("base_url", "http://localhost:8000"),
            timeout=config_obj.get("timeout", 30),
        )

    # Get database from ports if database mode
    if mode in (SeedMode.DATABASE, SeedMode.HYBRID):
        ports = _parse_ports((), config_obj)
        database = ports.get("database")

    seed_manager = SeedManager(client=client, database=database, config=config)

    try:
        seeds = seed_manager.load(seed_file)

        if dry_run:
            click.echo(f"Dry run - would seed from: {seed_file}")
            click.echo(f"  Resource types: {', '.join(seeds.resource_types)}")
            click.echo(f"  Total resources: {seeds.total_resources}")
            click.echo(f"  Mode: {mode.value}")
            click.echo(f"  Prefix: {config.prefix}")
            return

        result = seed_manager.apply(seeds, mode=mode, prefix=prefix)

        if output_format == "json":
            import json

            output_data = {
                "success": result.success,
                "created_count": result.created_count,
                "failed_count": result.failed_count,
                "prefix": result.prefix,
                "duration_ms": result.duration_ms,
                "created_resources": [
                    {
                        "type": r.resource_type,
                        "logical_id": r.logical_id,
                        "actual_id": r.actual_id,
                    }
                    for r in result.created_resources
                ],
                "failed_resources": [
                    {"type": r.resource_type, "logical_id": r.logical_id, "error": err}
                    for r, err in result.failed_resources
                ],
            }
            click.echo(json.dumps(output_data, indent=2, default=str))
        else:
            if result.success:
                click.echo(f"Successfully seeded {result.created_count} resources")
            else:
                click.echo(
                    f"Seeding completed with errors: "
                    f"{result.created_count} created, {result.failed_count} failed"
                )

            click.echo(f"  Prefix: {result.prefix}")
            click.echo(f"  Duration: {result.duration_ms:.0f}ms")

            if result.failed_resources:
                click.echo("\nFailed resources:")
                for resource, error in result.failed_resources:
                    click.echo(
                        f"  - {resource.resource_type}/{resource.logical_id}: {error}"
                    )

        sys.exit(0 if result.success else 1)

    except FileNotFoundError as e:
        click.echo(f"Seed file not found: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Seeding failed: {e}", err=True)
        if ctx.obj.get("verbose"):
            import traceback

            traceback.print_exc()
        sys.exit(1)


@cli.command("security-scan")
@click.option(
    "--target",
    "-t",
    "target_url",
    required=True,
    help="Target URL to scan (e.g., http://localhost:8000)",
)
@click.option(
    "--endpoint",
    "-e",
    "endpoints",
    multiple=True,
    help="Endpoints to test (can be specified multiple times)",
)
@click.option(
    "--auth-endpoint",
    "auth_endpoints",
    multiple=True,
    help="Authentication endpoints for rate limit testing",
)
@click.option(
    "--token",
    "user_token",
    default=None,
    help="User token for authenticated tests",
)
@click.option(
    "--admin-endpoint",
    "admin_endpoints",
    multiple=True,
    help="Admin endpoints for privilege escalation tests",
)
@click.option(
    "--skip",
    "skip_tests",
    multiple=True,
    type=click.Choice(["authentication", "injection", "owasp"]),
    help="Test categories to skip",
)
@click.option(
    "--max-payloads",
    default=50,
    type=int,
    help="Maximum payloads per test category (default: 50)",
)
@click.option(
    "--timeout",
    default=30.0,
    type=float,
    help="Request timeout in seconds (default: 30)",
)
@click.option(
    "--output",
    "-o",
    "output_path",
    type=click.Path(),
    help="Output file path",
)
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["sarif", "json", "markdown", "text"]),
    default="text",
    help="Output format (default: text)",
)
@click.option(
    "--fail-on-high",
    is_flag=True,
    help="Exit with error code if high/critical vulnerabilities found",
)
@click.pass_context
def security_scan(
    ctx: click.Context,
    target_url: str,
    endpoints: tuple[str, ...],
    auth_endpoints: tuple[str, ...],
    user_token: str | None,
    admin_endpoints: tuple[str, ...],
    skip_tests: tuple[str, ...],
    max_payloads: int,
    timeout: float,
    output_path: str | None,
    output_format: str,
    fail_on_high: bool,
) -> None:
    """Run security vulnerability scan against a target.

    Performs comprehensive security testing including:
    - Authentication bypass testing
    - SQL injection detection
    - XSS vulnerability testing
    - Command injection testing
    - Security headers validation
    - CORS misconfiguration detection
    - Rate limiting verification
    - Error message leakage detection

    \b
    Examples:
        venomqa security-scan --target http://localhost:8000
        venomqa security-scan -t http://api.example.com -e /api/users -e /api/products
        venomqa security-scan -t http://localhost:8000 --format sarif -o report.sarif
        venomqa security-scan -t http://localhost:8000 --skip injection --format json
        venomqa security-scan -t http://api.example.com --token "Bearer jwt_here" --fail-on-high

    \b
    Output formats:
        text     - Human-readable console output (default)
        json     - Detailed JSON report
        sarif    - SARIF format for GitHub Code Scanning integration
        markdown - Markdown report for documentation
    """
    from venomqa import Client
    from venomqa.cli.output import CLIOutput, ProgressConfig
    from venomqa.domains.security import ScanConfig, SecurityScanner

    output = CLIOutput(ProgressConfig())

    # Default endpoints if none specified
    endpoint_list = list(endpoints) if endpoints else ["/", "/api"]
    auth_endpoint_list = list(auth_endpoints) if auth_endpoints else ["/api/login", "/api/auth/token"]
    admin_endpoint_list = list(admin_endpoints) if admin_endpoints else ["/api/admin"]

    output.console.print("\n[bold cyan]VenomQA Security Scanner[/bold cyan]")
    output.console.print(f"  Target: {target_url}")
    output.console.print(f"  Endpoints: {', '.join(endpoint_list)}")
    output.console.print(f"  Max payloads: {max_payloads}")
    if skip_tests:
        output.console.print(f"  Skipping: {', '.join(skip_tests)}")
    output.console.print()

    # Create client
    client = Client(base_url=target_url, timeout=timeout)

    # Create scan config
    scan_config = ScanConfig(
        target_url=target_url,
        endpoints=endpoint_list,
        auth_endpoints=auth_endpoint_list,
        admin_endpoints=admin_endpoint_list,
        user_token=user_token,
        timeout=timeout,
        max_payloads=max_payloads,
        skip_tests=list(skip_tests),
    )

    # Run scan
    scanner = SecurityScanner(client)
    output.console.print("[bold]Starting security scan...[/bold]\n")

    try:
        result = scanner.run_full_scan(scan_config)
    except Exception as e:
        output.console.print(f"[red]Scan failed: {e}[/red]")
        sys.exit(1)

    # Generate output
    if output_format == "text":
        _display_security_results(output, result)
    elif output_format == "sarif":
        if output_path:
            scanner.generate_sarif_report(result, output_path)
            output.console.print(f"\n[green]SARIF report saved to: {output_path}[/green]")
        else:
            import json

            sarif = _generate_sarif_inline(result)
            click.echo(json.dumps(sarif, indent=2))
    elif output_format == "json":
        if output_path:
            scanner.generate_json_report(result, output_path)
            output.console.print(f"\n[green]JSON report saved to: {output_path}[/green]")
        else:
            import json

            report = _generate_json_inline(result)
            click.echo(json.dumps(report, indent=2, default=str))
    elif output_format == "markdown":
        if output_path:
            scanner.generate_markdown_report(result, output_path)
            output.console.print(f"\n[green]Markdown report saved to: {output_path}[/green]")
        else:
            md = _generate_markdown_inline(result)
            click.echo(md)

    # Save report if output path specified but not already saved
    if output_path and output_format == "text":
        scanner.generate_markdown_report(result, output_path.replace(".txt", ".md"))
        output.console.print(f"\n[green]Report saved to: {output_path.replace('.txt', '.md')}[/green]")

    # Exit with error if high severity issues found and flag is set
    if fail_on_high and result.has_critical_issues:
        output.console.print("\n[red]High/Critical vulnerabilities found. Failing build.[/red]")
        sys.exit(1)

    sys.exit(0)


def _display_security_results(output: Any, result: Any) -> None:
    """Display security scan results in terminal."""
    output.console.print("[bold]Scan Complete[/bold]")
    output.console.print(f"  Duration: {result.scan_duration_ms:.0f}ms")
    output.console.print(f"  Total findings: {result.total_findings}")
    output.console.print()

    if result.findings_by_severity:
        output.console.print("[bold]Findings by Severity:[/bold]")
        severity_colors = {
            "critical": "red",
            "high": "red",
            "medium": "yellow",
            "low": "blue",
            "info": "dim",
        }
        severity_order = ["critical", "high", "medium", "low", "info"]
        for severity in severity_order:
            count = result.findings_by_severity.get(severity, 0)
            if count > 0:
                color = severity_colors.get(severity, "white")
                output.console.print(f"  [{color}]{severity.upper()}: {count}[/{color}]")
        output.console.print()

    if result.all_findings:
        output.console.print("[bold]Vulnerability Details:[/bold]\n")
        for i, finding in enumerate(result.all_findings[:20], 1):  # Show first 20
            severity_colors = {
                "critical": "red",
                "high": "red",
                "medium": "yellow",
                "low": "blue",
                "info": "dim",
            }
            color = severity_colors.get(finding.severity.value, "white")
            output.console.print(f"[{color}]{i}. [{finding.severity.value.upper()}] {finding.title}[/{color}]")
            output.console.print(f"   Type: {finding.vuln_type.value}")
            output.console.print(f"   Location: {finding.location}")
            if finding.payload:
                payload_display = finding.payload[:80] + ("..." if len(finding.payload) > 80 else "")
                output.console.print(f"   Payload: {payload_display}")
            if finding.remediation:
                remediation_display = finding.remediation[:100] + "..." if len(finding.remediation) > 100 else finding.remediation
                output.console.print(f"   Remediation: {remediation_display}")
            output.console.print()

        if len(result.all_findings) > 20:
            output.console.print(f"[dim]... and {len(result.all_findings) - 20} more findings[/dim]")

    if result.errors:
        output.console.print("\n[yellow]Errors during scan:[/yellow]")
        for error in result.errors[:5]:
            output.console.print(f"  [yellow]- {error}[/yellow]")
        if len(result.errors) > 5:
            output.console.print(f"  [dim]... and {len(result.errors) - 5} more errors[/dim]")


def _generate_sarif_inline(result: Any) -> dict:
    """Generate SARIF report inline."""
    from venomqa.security.testing import VulnerabilitySeverity

    severity_map = {
        VulnerabilitySeverity.CRITICAL: "error",
        VulnerabilitySeverity.HIGH: "error",
        VulnerabilitySeverity.MEDIUM: "warning",
        VulnerabilitySeverity.LOW: "note",
        VulnerabilitySeverity.INFO: "none",
    }

    rules: dict[str, Any] = {}
    results_list: list[dict[str, Any]] = []

    for _i, finding in enumerate(result.all_findings):
        rule_id = f"VENOM-{finding.vuln_type.value.upper().replace('_', '-')}"

        if rule_id not in rules:
            rules[rule_id] = {
                "id": rule_id,
                "name": finding.vuln_type.value.replace("_", " ").title(),
                "shortDescription": {"text": finding.title},
                "defaultConfiguration": {"level": severity_map.get(finding.severity, "warning")},
            }

        results_list.append({
            "ruleId": rule_id,
            "level": severity_map.get(finding.severity, "warning"),
            "message": {"text": finding.description},
            "locations": [{"physicalLocation": {"artifactLocation": {"uri": finding.location or result.target}}}],
        })

    return {
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/schemas/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "VenomQA Security Scanner",
                    "version": "1.0.0",
                    "rules": list(rules.values()),
                }
            },
            "results": results_list,
        }],
    }


def _generate_json_inline(result: Any) -> dict:
    """Generate JSON report inline."""
    return {
        "scan_info": {
            "target": result.target,
            "started_at": result.scan_started.isoformat(),
            "finished_at": result.scan_finished.isoformat(),
            "duration_ms": result.scan_duration_ms,
        },
        "summary": {
            "total_findings": result.total_findings,
            "findings_by_severity": result.findings_by_severity,
            "is_vulnerable": result.is_vulnerable,
            "has_critical_issues": result.has_critical_issues,
        },
        "findings": [
            {
                "type": f.vuln_type.value,
                "severity": f.severity.value,
                "title": f.title,
                "description": f.description,
                "location": f.location,
                "remediation": f.remediation,
                "cwe": f.cwe,
                "owasp": f.owasp,
            }
            for f in result.all_findings
        ],
    }


def _generate_markdown_inline(result: Any) -> str:
    """Generate Markdown report inline."""
    lines = [
        "# VenomQA Security Scan Report\n",
        f"**Target:** {result.target}  ",
        f"**Scan Date:** {result.scan_started.strftime('%Y-%m-%d %H:%M:%S')}  ",
        f"**Duration:** {result.scan_duration_ms:.0f}ms\n",
        "## Summary\n",
        "| Severity | Count |",
        "|----------|-------|",
    ]
    for severity in ["critical", "high", "medium", "low", "info"]:
        count = result.findings_by_severity.get(severity, 0)
        lines.append(f"| {severity.upper()} | {count} |")

    if result.all_findings:
        lines.append("\n## Findings\n")
        for finding in result.all_findings:
            lines.extend([
                f"### [{finding.severity.value.upper()}] {finding.title}\n",
                f"**Type:** {finding.vuln_type.value}  ",
                f"**Location:** {finding.location}\n",
                f"{finding.description}\n",
                f"**Remediation:** {finding.remediation}\n",
                "---\n",
            ])

    return "\n".join(lines)


@cli.command()
@click.option(
    "--all",
    "-a",
    "cleanup_all",
    is_flag=True,
    help="Clean up all tracked resources",
)
@click.option(
    "--journey",
    "-j",
    "journey_name",
    default=None,
    help="Clean up resources for a specific journey",
)
@click.option(
    "--strategy",
    "-s",
    "strategy",
    type=click.Choice(["reverse_delete", "truncate", "snapshot_restore", "soft_delete"]),
    default="reverse_delete",
    help="Cleanup strategy to use",
)
@click.option(
    "--table",
    "-t",
    "tables",
    multiple=True,
    help="Tables to truncate (when using truncate strategy)",
)
@click.option(
    "--snapshot",
    "snapshot_name",
    default=None,
    help="Snapshot name to restore (when using snapshot_restore strategy)",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Show what would be cleaned up without actually cleaning",
)
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["text", "json"]),
    default="text",
    help="Output format",
)
@click.pass_context
def cleanup(
    ctx: click.Context,
    cleanup_all: bool,
    journey_name: str | None,
    strategy: str,
    tables: tuple[str, ...],
    snapshot_name: str | None,
    dry_run: bool,
    output_format: str,
) -> None:
    """Clean up test data.

    Removes test resources created during seeding or test execution.
    Supports multiple cleanup strategies.

    \b
    Examples:
        venomqa cleanup --all
        venomqa cleanup --journey checkout
        venomqa cleanup --strategy truncate --table users --table orders
        venomqa cleanup --strategy snapshot_restore --snapshot before_test

    \b
    Strategies:
        reverse_delete    - Delete resources in reverse order of creation (default)
        truncate          - Truncate specified tables
        snapshot_restore  - Restore database from a saved snapshot
        soft_delete       - Mark resources as deleted with TTL
    """
    from venomqa.data import CleanupConfig, CleanupManager, CleanupStrategy

    # Map strategy string to enum
    strategy_map = {
        "reverse_delete": CleanupStrategy.REVERSE_DELETE,
        "truncate": CleanupStrategy.TRUNCATE,
        "snapshot_restore": CleanupStrategy.SNAPSHOT_RESTORE,
        "soft_delete": CleanupStrategy.SOFT_DELETE,
    }
    cleanup_strategy = strategy_map[strategy]

    config = CleanupConfig(strategy=cleanup_strategy)

    # Get client and database from config
    config_obj: dict[str, Any] = ctx.obj.get("config", {})
    client = None
    database = None

    # Create client for API cleanup
    from venomqa import Client

    client = Client(
        base_url=config_obj.get("base_url", "http://localhost:8000"),
        timeout=config_obj.get("timeout", 30),
    )

    # Get database from ports
    ports = _parse_ports((), config_obj)
    database = ports.get("database")

    cleanup_manager = CleanupManager(client=client, database=database, config=config)

    # Check for stored seed manager in context
    seed_manager = ctx.obj.get("seed_manager")
    if seed_manager:
        cleanup_manager.register_resources(seed_manager.created_resources)

    try:
        if dry_run:
            resources = cleanup_manager.tracker.get_cleanup_order(journey_name)
            click.echo(f"Dry run - would clean up {len(resources)} resources")
            click.echo(f"  Strategy: {strategy}")
            if journey_name:
                click.echo(f"  Journey: {journey_name}")
            if resources:
                click.echo("\nResources to clean up:")
                for r in resources[:20]:  # Show first 20
                    click.echo(f"  - {r.resource_type}/{r.resource_id}")
                if len(resources) > 20:
                    click.echo(f"  ... and {len(resources) - 20} more")
            return

        # Special handling for truncate strategy
        if cleanup_strategy == CleanupStrategy.TRUNCATE and tables:
            if database is None:
                click.echo("Database connection required for truncate strategy", err=True)
                sys.exit(1)

            for table in tables:
                database.truncate(table, cascade=True)
                click.echo(f"Truncated table: {table}")
            sys.exit(0)

        # Special handling for snapshot restore
        if cleanup_strategy == CleanupStrategy.SNAPSHOT_RESTORE:
            if snapshot_name is None:
                click.echo("Snapshot name required for snapshot_restore strategy", err=True)
                sys.exit(1)

            try:
                cleanup_manager.restore_snapshot(snapshot_name)
                click.echo(f"Restored from snapshot: {snapshot_name}")
                sys.exit(0)
            except ValueError as e:
                click.echo(str(e), err=True)
                sys.exit(1)

        # Standard cleanup
        result = cleanup_manager.cleanup(
            journey=journey_name,
            strategy=cleanup_strategy,
        )

        if output_format == "json":
            import json

            output_data = {
                "success": result.success,
                "deleted_count": result.deleted_count,
                "failed_count": result.failed_count,
                "strategy": result.strategy.value,
                "duration_ms": result.duration_ms,
                "deleted_resources": [
                    {"type": r.resource_type, "id": r.resource_id}
                    for r in result.deleted_resources
                ],
                "failed_resources": [
                    {"type": r.resource_type, "id": r.resource_id, "error": err}
                    for r, err in result.failed_resources
                ],
            }
            click.echo(json.dumps(output_data, indent=2, default=str))
        else:
            if result.success:
                click.echo(f"Successfully cleaned up {result.deleted_count} resources")
            else:
                click.echo(
                    f"Cleanup completed with errors: "
                    f"{result.deleted_count} deleted, {result.failed_count} failed"
                )

            click.echo(f"  Strategy: {result.strategy.value}")
            click.echo(f"  Duration: {result.duration_ms:.0f}ms")

            if result.failed_resources:
                click.echo("\nFailed to clean up:")
                for resource, error in result.failed_resources:
                    click.echo(f"  - {resource.resource_type}/{resource.resource_id}: {error}")

        sys.exit(0 if result.success else 1)

    except Exception as e:
        click.echo(f"Cleanup failed: {e}", err=True)
        if ctx.obj.get("verbose"):
            import traceback

            traceback.print_exc()
        sys.exit(1)


# ── V1 bridge commands ──────────────────────────────────────────────────────


@cli.command("explore")
@click.argument("journey_file", type=click.Path(exists=True))
@click.option("--base-url", "-u", required=True, help="Base URL of API")
@click.option("--db-url", default=None, help="PostgreSQL connection string")
@click.option("--redis-url", default=None, help="Redis connection string")
@click.option(
    "--strategy",
    type=click.Choice(["bfs", "dfs", "random"]),
    default="bfs",
    show_default=True,
)
@click.option("--max-steps", type=int, default=1000, show_default=True)
@click.option(
    "--format",
    "-f",
    "output_format",
    type=click.Choice(["console", "json", "markdown", "junit"]),
    default="console",
    show_default=True,
)
@click.option("--output", "-o", default=None, help="Output file (default: stdout)")
def explore_v1(
    journey_file: str,
    base_url: str,
    db_url: str | None,
    redis_url: str | None,
    strategy: str,
    max_steps: int,
    output_format: str,
    output: str | None,
) -> None:
    """Run stateful V1 exploration against an API."""
    import types

    from venomqa.v1.cli.main import cmd_explore

    args = types.SimpleNamespace(
        journey_file=journey_file,
        base_url=base_url,
        db_url=db_url,
        redis_url=redis_url,
        strategy=strategy,
        max_steps=max_steps,
        format=output_format,
        output=output,
    )
    sys.exit(cmd_explore(args))


@cli.command("validate")
@click.argument("journey_file", type=click.Path(exists=True))
def validate_v1(journey_file: str) -> None:
    """Validate V1 journey syntax without running it."""
    import types

    from venomqa.v1.cli.main import cmd_validate

    args = types.SimpleNamespace(journey_file=journey_file)
    sys.exit(cmd_validate(args))


@cli.command("record")
@click.argument("journey_file")
@click.option("--base-url", "-u", required=True, help="Base URL of API to record")
@click.option("--output", "-o", default=None, help="Output file for generated code (default: stdout)")
@click.option("--name", default="recorded_journey", show_default=True, help="Journey name in generated code")
def record_v1(
    journey_file: str,
    base_url: str,
    output: str | None,
    name: str,
) -> None:
    """Proxy HTTP calls and generate a V1 Journey skeleton."""
    import types

    from venomqa.v1.cli.main import cmd_record

    args = types.SimpleNamespace(
        journey_file=journey_file,
        base_url=base_url,
        output=output,
        name=name,
    )
    sys.exit(cmd_record(args))

