"""Tests for DER-related model fields: StepOutcome, IssueClassification, JobStep, EvidenceAttachment.

Covers enum membership, EvidenceAttachment recording_timestamp variant,
shell_output truncation boundary, and JobStep DER field persistence.
"""

from __future__ import annotations

import pytest
from pydantic import ValidationError

from mcp_vm_blackbox.models import (
    EVIDENCE_CONTENT_MAX_CHARS,
    EvidenceAttachment,
    IssueClassification,
    JobStep,
    ResolutionType,
    StepOutcome,
)

# ---------------------------------------------------------------------------
# StepOutcome enum
# ---------------------------------------------------------------------------


class TestStepOutcomeEnum:
    """Verify StepOutcome has the required members and string values."""

    def test_member_count(self) -> None:
        """StepOutcome must have exactly 4 members.

        Tests: StepOutcome enum completeness.
        How: Assert len(StepOutcome) equals 4.
        Why: Adding or removing members silently would break existing serialized
            job JSON and agent code that dispatches on outcome_status.
        """
        assert len(StepOutcome) == 4

    def test_all_expected_values(self) -> None:
        """StepOutcome contains success, failed, blocked, skipped.

        Tests: Exact enum value set for StepOutcome.
        How: Collect all member values into a set and compare to the expected set.
        Why: vm_job_step validates outcome_status against these values; any mismatch
            produces a ToolError that blocks agents from logging step outcomes.
        """
        values = {member.value for member in StepOutcome}
        assert values == {"success", "failed", "blocked", "skipped"}

    def test_is_str_enum(self) -> None:
        """StepOutcome values compare equal to plain strings.

        Tests: StrEnum inheritance so values are usable in JSON serialization.
        How: Assert StepOutcome.SUCCESS == "success" using direct equality.
        Why: Pydantic serializes StrEnum fields as their string value; if the
            type were IntEnum or plain Enum this would break JSON output.
        """
        assert StepOutcome.SUCCESS == "success"
        assert StepOutcome.FAILED == "failed"
        assert StepOutcome.BLOCKED == "blocked"
        assert StepOutcome.SKIPPED == "skipped"


# ---------------------------------------------------------------------------
# IssueClassification enum
# ---------------------------------------------------------------------------


class TestIssueClassificationEnum:
    """Verify IssueClassification has the required members and string values."""

    def test_member_count(self) -> None:
        """IssueClassification must have exactly 3 members.

        Tests: IssueClassification enum completeness.
        How: Assert len(IssueClassification) equals 3.
        Why: The harness boundary test relies on exactly these three categories;
            drift silently breaks triage logic and post-mortem rendering.
        """
        assert len(IssueClassification) == 3

    def test_all_expected_values(self) -> None:
        """IssueClassification contains user-facing, harness, environment.

        Tests: Exact enum value set for IssueClassification.
        How: Collect all member values into a set and compare to the expected set.
        Why: vm_job_step validates issue_classification against these values.
        """
        values = {member.value for member in IssueClassification}
        assert values == {"user-facing", "harness", "environment"}

    def test_is_str_enum(self) -> None:
        """IssueClassification values compare equal to plain strings.

        Tests: StrEnum inheritance so values serialize correctly to JSON.
        How: Assert each member equals its expected string literal.
        Why: Pydantic serializes StrEnum fields as their string value.
        """
        assert IssueClassification.USER_FACING == "user-facing"
        assert IssueClassification.HARNESS == "harness"
        assert IssueClassification.ENVIRONMENT == "environment"


# ---------------------------------------------------------------------------
# ResolutionType enum
# ---------------------------------------------------------------------------


class TestResolutionType:
    """Verify ResolutionType has the required members and string values."""

    def test_member_count(self) -> None:
        """ResolutionType must have exactly 4 members.

        Tests: ResolutionType enum completeness.
        How: Assert len(ResolutionType) equals 4.
        Why: Adding or removing members silently would break existing serialized
            issue JSON and agent code that dispatches on resolution_type.
        """
        assert len(ResolutionType) == 4

    def test_all_expected_values(self) -> None:
        """ResolutionType contains fixed, workaround, escalated, deferred.

        Tests: Exact enum value set for ResolutionType.
        How: Assert each member equals its expected string literal.
        Why: vm_issue_create validates resolution_type against these values;
            any mismatch produces a ToolError that blocks issue resolution.
        """
        assert ResolutionType.FIXED == "fixed"
        assert ResolutionType.WORKAROUND == "workaround"
        assert ResolutionType.ESCALATED == "escalated"
        assert ResolutionType.DEFERRED == "deferred"

    def test_is_str_enum(self) -> None:
        """ResolutionType values compare equal to plain strings.

        Tests: StrEnum inheritance so values serialize correctly to JSON.
        How: Assert issubclass(ResolutionType, str).
        Why: Pydantic serializes StrEnum fields as their string value; if the
            type were plain Enum this would break JSON output.
        """
        assert issubclass(ResolutionType, str)


# ---------------------------------------------------------------------------
# EvidenceAttachment: shell_output boundary
# ---------------------------------------------------------------------------


class TestEvidenceAttachmentShellOutput:
    """Validate shell_output content length enforcement."""

    def test_shell_output_at_exactly_max_chars_accepted(self) -> None:
        """shell_output content at exactly EVIDENCE_CONTENT_MAX_CHARS is stored unchanged.

        Tests: Boundary condition at the maximum allowed length for shell_output.
        How: Build a string of exactly EVIDENCE_CONTENT_MAX_CHARS characters, pass
            it as content, and assert the stored content equals the input.
        Why: Off-by-one in the truncation guard would silently discard the final
            character of content that is exactly at the limit.
        """
        # Arrange
        content = "x" * EVIDENCE_CONTENT_MAX_CHARS

        # Act
        attachment = EvidenceAttachment(type="shell_output", content=content)

        # Assert
        assert attachment.content is not None
        assert len(attachment.content) == EVIDENCE_CONTENT_MAX_CHARS
        assert attachment.content == content

    def test_shell_output_exceeding_max_chars_is_truncated(self) -> None:
        """shell_output content longer than EVIDENCE_CONTENT_MAX_CHARS is truncated.

        Tests: Auto-truncation when shell_output content exceeds the 4096-char limit.
        How: Build a string of EVIDENCE_CONTENT_MAX_CHARS + 100 characters, pass it
            as content, and assert the stored content is truncated to exactly
            EVIDENCE_CONTENT_MAX_CHARS characters preserving the head.
        Why: Unbounded inline evidence would bloat job JSON files; truncation prevents
            storage overflow while preserving the most useful leading content.
        """
        # Arrange
        over_limit = EVIDENCE_CONTENT_MAX_CHARS + 100
        content = "a" * over_limit

        # Act
        attachment = EvidenceAttachment(type="shell_output", content=content)

        # Assert
        assert attachment.content is not None
        assert len(attachment.content) == EVIDENCE_CONTENT_MAX_CHARS
        assert attachment.content == "a" * EVIDENCE_CONTENT_MAX_CHARS


# ---------------------------------------------------------------------------
# EvidenceAttachment: recording_timestamp
# ---------------------------------------------------------------------------


class TestEvidenceAttachmentRecordingTimestamp:
    """Validate the recording_timestamp evidence kind."""

    def test_recording_timestamp_with_content_is_stored(self) -> None:
        """recording_timestamp evidence with an ISO-8601 timestamp in content is accepted.

        Tests: New recording_timestamp variant added to EvidenceAttachment.type Literal.
        How: Construct an EvidenceAttachment with type='recording_timestamp' and an
            ISO-8601 timestamp string in content; assert type and content are preserved.
        Why: Agents use recording_timestamp to correlate step events with video frames
            for summary video generation; missing validation would silently drop entries.
        """
        # Arrange / Act
        attachment = EvidenceAttachment(type="recording_timestamp", content="2026-03-20T14:03:22Z")

        # Assert
        assert attachment.type == "recording_timestamp"
        assert attachment.content == "2026-03-20T14:03:22Z"
        assert attachment.path is None

    def test_recording_timestamp_without_content_raises(self) -> None:
        """recording_timestamp evidence without content raises ValidationError.

        Tests: Mandatory content requirement for recording_timestamp type.
        How: Attempt to construct EvidenceAttachment(type='recording_timestamp')
            without a content field and assert ValidationError is raised.
        Why: A recording_timestamp without a timestamp value is meaningless; the
            model validator must reject it rather than store an empty record.
        """
        # Act / Assert
        with pytest.raises(ValidationError):
            EvidenceAttachment(type="recording_timestamp")


# ---------------------------------------------------------------------------
# JobStep DER fields
# ---------------------------------------------------------------------------


class TestJobStepDerFields:
    """Validate DER field storage on JobStep model instances."""

    def test_job_step_all_der_fields_stored(self) -> None:
        """JobStep stores all seven DER fields when provided.

        Tests: DER field persistence on the JobStep model.
        How: Construct a JobStep with all seven DER fields populated, then assert
            each field equals the value that was passed in.
        Why: If any DER field were silently dropped or not mapped, the post-mortem
            renderer would produce 'N/A' output instead of the agent's actual data.
        """
        # Arrange / Act
        step = JobStep(
            agent_id="agent-1",
            action="install service",
            timestamp="2026-03-20T14:00:00Z",
            intent="Install the Windows service",
            reason="Required by the scenario goal state",
            outcome_status=StepOutcome.SUCCESS,
            outcome_detail="Service started on first attempt",
            issue_classification=None,
            resolution=None,
            next="Verify service is running via vm_powershell",
        )

        # Assert
        assert step.intent == "Install the Windows service"
        assert step.reason == "Required by the scenario goal state"
        assert step.outcome_status == StepOutcome.SUCCESS
        assert step.outcome_detail == "Service started on first attempt"
        assert step.issue_classification is None
        assert step.resolution is None
        assert step.next == "Verify service is running via vm_powershell"

    def test_job_step_der_fields_default_to_none(self) -> None:
        """JobStep DER fields default to None when not provided.

        Tests: Backward-compatible defaults for all seven DER fields on JobStep.
        How: Construct a JobStep with only the required fields (agent_id, action,
            timestamp) and assert all seven DER fields are None.
        Why: Existing serialized job JSON files omit DER fields; Pydantic must
            deserialize them without error and produce None for absent fields.
        """
        # Arrange / Act
        step = JobStep(agent_id="agent-1", action="legacy step", timestamp="2026-03-20T14:00:00Z")

        # Assert
        assert step.intent is None
        assert step.reason is None
        assert step.outcome_status is None
        assert step.outcome_detail is None
        assert step.issue_classification is None
        assert step.resolution is None
        assert step.next is None

    def test_job_step_issue_classification_stored(self) -> None:
        """JobStep stores IssueClassification enum value on the issue_classification field.

        Tests: issue_classification field accepts IssueClassification enum values.
        How: Construct a JobStep with issue_classification=IssueClassification.HARNESS
            and assert the stored field equals that enum value.
        Why: Issue triage depends on issue_classification being stored with the correct
            enum type so post-mortem and inspector tools can filter by category.
        """
        # Arrange / Act
        step = JobStep(
            agent_id="agent-1",
            action="provision failed",
            timestamp="2026-03-20T14:00:00Z",
            outcome_status=StepOutcome.FAILED,
            issue_classification=IssueClassification.HARNESS,
            resolution="Reported to plugin team",
        )

        # Assert
        assert step.issue_classification == IssueClassification.HARNESS
        assert step.issue_classification == "harness"
        assert step.resolution == "Reported to plugin team"
