"""Step definitions for domain models testing."""

from datetime import datetime
from decimal import Decimal
from pathlib import Path

from behave import given, then, when
from pydantic import ValidationError

from cleveragents.domain.models.aimodels_custom import CustomAIModel
from cleveragents.domain.models.aimodelsdatamodels import AIModel, Provider
from cleveragents.domain.models.core import (
    Change,
    ChangeSet,
    CloudBillingFields,
    Context,
    ContextType,
    ContextUpdateResult,
    CreditsTransaction,
    CreditsTransactionType,
    CreditType,
    Invite,
    OperationType,
    Org,
    OrgRole,
    OrgUser,
    Plan,
    PlanStatus,
    Project,
    ProjectSettings,
    SummaryForUpdateContextParams,
    User,
)
from cleveragents.domain.models.core.enums import ModelProvider
from cleveragents.domain.models.orguserconfig import OrgUserConfig
from cleveragents.domain.models.planconfig import PlanConfig


def _sample_plan_config() -> PlanConfig:
    """Create a reusable PlanConfig instance for tests."""

    return PlanConfig(
        auto_mode="manual",
        editor="vim",
        editor_command="vim",
        editor_args=[],
        editor_open_manually=False,
        auto_continue=False,
        auto_build=False,
        auto_update_context=False,
        auto_context=False,
        smart_context=False,
        auto_apply=False,
        auto_commit=False,
        skip_commit=False,
        can_exec=False,
        auto_exec=False,
        auto_debug=False,
        auto_debug_tries=0,
        auto_revert_on_rewind=False,
        skip_changes_menu=False,
    )


@given("I have valid project data")
def step_valid_project_data(context):
    """Set up valid project data."""
    context.project_data = {
        "name": "test-project",
        "path": Path("/tmp/test-project"),
        "settings": ProjectSettings(),
    }


@given('I have project data with invalid name "{name}"')
def step_invalid_project_name(context, name):
    """Set up project data with invalid name."""
    context.project_data = {
        "name": name,
        "path": Path("/tmp/test-project"),
    }


@when("I create a Project model")
def step_create_project(context):
    """Create a Project model from data."""
    try:
        context.project = Project(**context.project_data)
        context.error = None
    except ValidationError as e:
        context.project = None
        context.error = e


@when("I try to create a Project model")
def step_try_create_project(context):
    """Try to create a Project model, expecting it might fail."""
    try:
        context.project = Project(**context.project_data)
        context.error = None
    except ValidationError as e:
        context.project = None
        context.error = e


@then("the project model should be created successfully")
def step_project_created(context):
    """Check that project was created."""
    assert context.project is not None
    assert context.error is None


@then("the project name should be validated")
def step_project_name_validated(context):
    """Check that project name is valid."""
    assert context.project.name == context.project_data["name"]


@then("the project path should be absolute")
def step_project_path_absolute(context):
    """Check that project path is absolute."""
    assert context.project.path.is_absolute()


@then("a validation error should be raised")
def step_validation_error_raised(context):
    """Check that validation error was raised."""
    assert context.error is not None
    assert isinstance(context.error, ValidationError)


@then('the error should mention "{message}"')
def step_error_message(context, message):
    """Check that error contains expected message."""
    assert message in str(context.error)


# Plan-related steps
@given("I have valid plan data")
def step_valid_plan_data(context):
    """Set up valid plan data."""
    context.plan_data = {
        "project_id": 1,
        "name": "test-plan",
        "prompt": "Add error handling",
        "status": PlanStatus.PENDING,
    }


@when("I create a Plan model")
def step_create_plan(context):
    """Create a Plan model from data."""
    context.plan = Plan(**context.plan_data)


@then("the plan model should be created successfully")
def step_plan_created(context):
    """Check that plan was created."""
    assert context.plan is not None


@then('the plan status should be "{status}"')
def step_plan_status(context, status):
    """Check plan status."""
    assert context.plan.status == status


@then("the plan should have timestamps")
def step_plan_timestamps(context):
    """Check that plan has timestamps."""
    assert isinstance(context.plan.created_at, datetime)
    assert isinstance(context.plan.updated_at, datetime)


@given('I have a plan with status "{status}"')
def step_plan_with_status(context, status):
    """Create a plan with specific status."""
    context.plan = Plan(
        project_id=1,
        name="test-plan",
        prompt="Test",
        status=PlanStatus(status),
    )


@when('I update the plan status to "{status}"')
def step_update_plan_status(context, status):
    """Update plan status."""
    context.plan.status = PlanStatus(status)


@then('the updated plan status should be "{status}"')
def step_check_updated_plan_status(context, status):
    """Check updated plan status."""
    assert context.plan.status == status


@then("the updated_at timestamp should be updated")
def step_check_updated_timestamp(context):
    """Check that updated_at is set."""
    # In real implementation, this would be handled by the repository
    assert context.plan.updated_at is not None


# Context-related steps
@given("I have valid context data")
def step_valid_context_data(context):
    """Set up valid context data."""
    context.context_data = {
        "plan_id": 1,
        "type": ContextType.FILE,
        "path": "/tmp/test.py",
        "size": 1024,
    }


@when("I create a Context model")
def step_create_context(context):
    """Create a Context model from data."""
    context.context_obj = Context(**context.context_data)


@then("the context model should be created successfully")
def step_context_created(context):
    """Check that context was created."""
    assert context.context_obj is not None


@then('the context type should be "{type_name}"')
def step_context_type(context, type_name):
    """Check context type."""
    assert context.context_obj.type == type_name


@then("the context should have a path")
def step_context_path(context):
    """Check that context has a path."""
    assert context.context_obj.path is not None
    assert len(context.context_obj.path) > 0


@given("I have context update statistics")
def step_context_update_stats(context):
    """Prepare representative context update statistics."""
    context.context_update_data = {
        "updatedContexts": [
            {
                "plan_id": 1,
                "type": ContextType.FILE,
                "path": "/tmp/main.py",
                "size": 512,
            },
            {
                "plan_id": 1,
                "type": ContextType.URL,
                "path": "https://example.com/docs",
                "size": 0,
            },
        ],
        "tokenDiffsById": {
            "ctx-1": {"added": 120, "removed": 20},
            "ctx-2": {"added": 0, "removed": 0},
        },
        "tokensDiff": 100,
        "totalTokens": 640,
        "numFiles": 1,
        "numUrls": 1,
        "numImages": 0,
        "numTrees": 0,
        "numMaps": 0,
        "maxTokens": 4096,
    }


@when("I create a ContextUpdateResult summary")
def step_create_context_update_result(context):
    """Instantiate a ContextUpdateResult model from prepared data."""
    context.context_update_result = ContextUpdateResult(**context.context_update_data)


@then("the context update summary should report correct totals")
def step_context_update_totals(context):
    """Verify the aggregated counts returned by ContextUpdateResult."""
    result = context.context_update_result
    assert len(result.updated_contexts) == 2
    assert result.num_files == 1
    assert result.num_urls == 1
    assert result.num_images == 0
    assert result.tokens_diff == 100
    assert result.total_tokens == 640


@then("the token diffs should remain non-negative")
def step_context_update_token_diff_non_negative(context):
    """Ensure nested token diff counters never drop below zero."""
    result = context.context_update_result
    for diff_map in result.token_diffs_by_id.values():
        assert all(value >= 0 for value in diff_map.values())


@given("I have summary counts for context updates")
def step_summary_counts_context_updates(context):
    """Prepare summary counts mimicking camelCase payloads."""
    context.context_summary_data = {
        "numFiles": 3,
        "numTrees": 1,
        "numUrls": 2,
        "numMaps": 0,
        "tokensDiff": 25,
        "totalTokens": 512,
    }


@when("I create a SummaryForUpdateContextParams model")
def step_create_summary_update_context_params(context):
    """Create SummaryForUpdateContextParams from provided counts."""
    context.context_summary = SummaryForUpdateContextParams(
        **context.context_summary_data
    )


@then("the summary counts should match the source data")
def step_verify_summary_update_context(context):
    """Validate that camelCase keys hydrate correctly into the model."""
    summary = context.context_summary
    assert summary.num_files == 3
    assert summary.num_trees == 1
    assert summary.num_urls == 2
    assert summary.num_maps == 0
    assert summary.tokens_diff == 25
    assert summary.total_tokens == 512


# Change-related steps
@given("I have valid change data")
def step_valid_change_data(context):
    """Set up valid change data."""
    context.change_data = {
        "plan_id": 1,
        "file_path": "/tmp/test.py",
        "operation": OperationType.MODIFY,
        "original_content": "old content",
        "new_content": "new content",
    }


@when("I create a Change model")
def step_create_change(context):
    """Create a Change model from data."""
    context.change = Change(**context.change_data)


@then("the change model should be created successfully")
def step_change_created(context):
    """Check that change was created."""
    assert context.change is not None


@then("the change operation should be valid")
def step_change_operation_valid(context):
    """Check that change operation is valid."""
    assert context.change.operation in OperationType


@then("the change should not be applied by default")
def step_change_not_applied(context):
    """Check that change is not applied by default."""
    assert context.change.applied is False


# ChangeSet-related steps
@given("I have a changeset with multiple changes")
def step_changeset_with_changes(context):
    """Create a changeset with multiple changes."""
    changes = [
        Change(
            plan_id=1,
            file_path="/tmp/file1.py",
            operation=OperationType.CREATE,
            new_content="new file",
        ),
        Change(
            plan_id=1,
            file_path="/tmp/file2.py",
            operation=OperationType.MODIFY,
            original_content="old",
            new_content="new",
        ),
        Change(
            plan_id=1,
            file_path="/tmp/file3.py",
            operation=OperationType.DELETE,
            original_content="deleted content",
        ),
    ]
    context.changeset = ChangeSet(plan_id=1, changes=changes)


@when("I get the changeset statistics")
def step_get_changeset_stats(context):
    """Get statistics from changeset."""
    context.stats = context.changeset.stats


@then("the stats should show correct counts")
def step_stats_correct_counts(context):
    """Check that stats show correct counts."""
    assert context.stats["total"] == 3
    assert context.stats["creates"] == 1
    assert context.stats["modifies"] == 1
    assert context.stats["deletes"] == 1


@then("the stats should count creates, modifies, and deletes")
def step_stats_count_operations(context):
    """Check that stats count all operation types."""
    assert "creates" in context.stats
    assert "modifies" in context.stats
    assert "deletes" in context.stats
    assert "applied" in context.stats


# Domain models from cloud/organization data


def _sample_billing_payload() -> dict[str, object]:
    """Provide representative camelCase billing payload."""

    return {
        "creditsBalance": "125.5",
        "monthlyGrant": "200.0",
        "autoRebuyEnabled": True,
        "autoRebuyMinThreshold": "5.0",
        "autoRebuyToBalance": "50.0",
        "notifyThreshold": "10.0",
        "maxThresholdPerMonth": "500.0",
        "billingCycleStartedAt": "2025-01-01T00:00:00Z",
        "changedBillingMode": False,
        "trialPaid": True,
        "stripeSubscriptionId": "sub-123",
        "subscriptionStatus": "active",
        "subscriptionPausedAt": "2025-01-10T00:00:00Z",
        "stripePaymentMethod": "pm_abc",
        "subscriptionActionRequired": False,
        "subscriptionActionRequiredInvoiceUrl": None,
    }


@given("I have sample cloud billing data")
def step_sample_cloud_billing(context):
    """Prepare representative cloud billing data."""

    context.cloud_billing_data = _sample_billing_payload()


@when("I create a CloudBillingFields model")
def step_create_billing_fields(context):
    """Instantiate CloudBillingFields from camelCase payload."""

    context.billing_fields = CloudBillingFields(**context.cloud_billing_data)


@then("the billing fields should expose decimal values")
def step_verify_billing_fields(context):
    """Validate decimal conversion for billing fields."""

    billing = context.billing_fields
    assert billing.credits_balance == Decimal("125.5")
    assert billing.monthly_grant == Decimal("200.0")
    assert billing.subscription_action_required is False


@given("I have organization data with billing")
def step_org_with_billing(context):
    """Prepare organization payload referencing billing fields."""

    context.org_payload = {
        "id": "org-1",
        "name": "Example Org",
        "isTrial": True,
        "autoAddDomainUsers": True,
        "integratedModelsMode": False,
    }
    context.org_billing_payload = _sample_billing_payload()


@when("I create an Org domain model")
def step_create_org_model(context):
    """Instantiate Org with nested billing fields."""

    billing = CloudBillingFields(**context.org_billing_payload)
    context.org_model = Org(cloud_billing_fields=billing, **context.org_payload)


@then("the org should include billing configuration")
def step_verify_org_model(context):
    """Ensure the org retains billing configuration."""

    org = context.org_model
    assert org.cloud_billing_fields is not None
    assert org.cloud_billing_fields.notify_threshold == Decimal("10.0")


@given("I have an org role definition")
def step_org_role_definition(context):
    """Set up org role payload."""

    context.org_role_data = {
        "id": "role-1",
        "isDefault": True,
        "label": "Owner",
        "description": "Full access",
    }


@when("I create an OrgRole model")
def step_create_org_role(context):
    """Instantiate OrgRole."""

    context.org_role = OrgRole(**context.org_role_data)


@then("the org role should retain its label")
def step_verify_org_role(context):
    """Ensure org role values persist."""

    assert context.org_role.label == "Owner"
    assert context.org_role.is_default is True


@given("I have user data referencing a default plan config")
def step_user_with_plan_config(context):
    """Prepare user payload referencing plan config."""

    context.user_plan_config = _sample_plan_config()
    context.user_data = {
        "id": "user-1",
        "name": "Casey",
        "email": "casey@example.com",
        "isTrial": False,
        "numNonDraftPlans": 3,
    }


@when("I create a User model")
def step_create_user_model(context):
    """Instantiate User with nested plan config."""

    context.user_model = User(
        default_plan_config=context.user_plan_config, **context.user_data
    )


@then("the user should include the default plan config")
def step_verify_user_model(context):
    """Ensure user retains plan config."""

    assert context.user_model.default_plan_config is not None
    assert context.user_model.default_plan_config.auto_mode == "manual"


@given("I have an org user association")
def step_org_user_association(context):
    """Prepare OrgUser payload."""

    context.org_user_config = OrgUserConfig(
        prompted_claude_max=False,
        use_claude_subscription=True,
        claude_subscription_cooldown_started_at=datetime(2025, 1, 1, 0, 0, 0),
    )
    context.org_user_data = {
        "orgId": "org-1",
        "userId": "user-1",
        "orgRoleId": "role-1",
    }


@when("I create an OrgUser model")
def step_create_org_user(context):
    """Instantiate OrgUser."""

    context.org_user_model = OrgUser(
        config=context.org_user_config, **context.org_user_data
    )


@then("the org user config should be attached")
def step_verify_org_user(context):
    """Ensure OrgUser carries config payload."""

    assert context.org_user_model.config is not None
    assert context.org_user_model.config.use_claude_subscription is True


@given("I have invite metadata")
def step_invite_metadata(context):
    """Prepare Invite payload."""

    context.invite_data = {
        "id": "invite-1",
        "orgId": "org-1",
        "email": "new@example.com",
        "name": "New User",
        "orgRoleId": "role-1",
        "inviterId": "user-1",
        "inviteeId": None,
        "acceptedAt": "2025-02-01T12:00:00Z",
        "createdAt": "2025-01-15T08:30:00Z",
    }


@when("I create an Invite model")
def step_create_invite(context):
    """Instantiate Invite."""

    context.invite_model = Invite(**context.invite_data)


@then("the invite should track acceptance timestamps")
def step_verify_invite(context):
    """Ensure Invite datetimes parsed."""

    assert isinstance(context.invite_model.accepted_at, datetime)
    assert context.invite_model.accepted_at.year == 2025


@given("I have credits transaction data")
def step_credits_transaction_data(context):
    """Prepare CreditsTransaction payload."""

    context.credits_transaction_data = {
        "id": "txn-1",
        "orgId": "org-1",
        "orgName": "Example Org",
        "userId": "user-1",
        "userEmail": "casey@example.com",
        "userName": "Casey",
        "transactionType": CreditsTransactionType.CREDIT,
        "amount": "42.0",
        "startBalance": "20.0",
        "endBalance": "62.0",
        "creditType": CreditType.TRIAL,
        "creditIsAutoRebuy": True,
        "creditAutoRebuyMinThreshold": "5.0",
        "creditAutoRebuyToBalance": "40.0",
        "debitModelProvider": ModelProvider.OPENAI,
        "debitModelRole": "ModelRolePlanner",
        "debitModelName": "gpt-4.1",
        "debitPlanId": "plan-1",
        "debitPlanName": "Core Plan",
        "createdAt": "2025-01-20T00:00:00Z",
    }


@when("I create a CreditsTransaction model")
def step_create_credits_transaction(context):
    """Instantiate CreditsTransaction."""

    context.credits_transaction = CreditsTransaction(**context.credits_transaction_data)


@then("the credits transaction should track balances")
def step_verify_credits_transaction(context):
    """Ensure CreditsTransaction stores decimals and provider metadata."""

    tx = context.credits_transaction
    assert tx.amount == Decimal("42.0")
    assert tx.debit_model_provider == ModelProvider.OPENAI
    assert tx.end_balance == Decimal("62.0")


# Auto-generated models steps
@when("I import the auto-generated models")
def step_import_autogenerated(context):
    """Import auto-generated models."""
    try:
        from cleveragents.domain.models.auth import AuthHeader
        from cleveragents.domain.models.planconfig import PlanConfig
        from cleveragents.domain.models.stream import BuildInfo

        context.import_error = None
        context.imported_models = [AuthHeader, BuildInfo, PlanConfig]
    except Exception as e:
        context.import_error = e
        context.imported_models = []


@then("all models should load without errors")
def step_models_load_without_errors(context):
    """Check that models loaded without errors."""
    assert context.import_error is None
    assert len(context.imported_models) > 0


@then("the models should have proper Pydantic configuration")
def step_models_have_pydantic_config(context):
    """Check that models have Pydantic configuration."""
    for model_class in context.imported_models:
        assert hasattr(model_class, "model_config")
        assert hasattr(model_class, "model_validate")


@given('I create an "{model_name}" with the following data:')
@given('I create a "{model_name}" with the following data:')
def create_model(context, model_name):
    data = {row["name"]: row["value"] for row in context.table}
    if model_name == "AIModel":
        context.model = AIModel(**data)
    elif model_name == "Provider":
        context.model = Provider(**data)
    elif model_name == "CustomAIModel":
        context.model = CustomAIModel(**data)


@then('the "{model_name}" object should have the following attributes:')
def verify_model_attributes(context, model_name):
    for row in context.table:
        attr_name = row["name"]
        expected_value = row["value"]
        actual_value = getattr(context.model, attr_name)
        if expected_value == "False":
            expected_value = False
        assert actual_value == expected_value, (
            f"Expected {attr_name} to be {expected_value}, but got {actual_value}"
        )
