"""Unit tests for metadata block schema validators.

Layer 3 (Pure Unit Tests): Tests for schema validation logic with zero dependencies.
Tests all validator methods for all schema classes in metadata.py.
"""

import pytest

from erk_shared.gateway.github.metadata.schemas import (
    CREATED_AT,
    CREATED_BY,
    CREATED_FROM_SESSION,
    LAST_DISPATCHED_AT,
    LAST_DISPATCHED_NODE_ID,
    LAST_DISPATCHED_RUN_ID,
    LAST_LEARN_AT,
    LAST_LEARN_SESSION,
    LAST_LOCAL_IMPL_AT,
    LAST_LOCAL_IMPL_EVENT,
    LAST_LOCAL_IMPL_SESSION,
    LAST_LOCAL_IMPL_USER,
    LAST_REMOTE_IMPL_AT,
    LAST_REMOTE_IMPL_RUN_ID,
    LAST_REMOTE_IMPL_SESSION_ID,
    OBJECTIVE_ISSUE,
    PLAN_COMMENT_ID,
    SCHEMA_VERSION,
    SOURCE_REPO,
    WORKTREE_NAME,
    ImplementationStatusSchema,
    PlanHeaderSchema,
    PlanSchema,
    SubmissionQueuedSchema,
    WorkflowStartedSchema,
    WorktreeCreationSchema,
)


class TestImplementationStatusSchema:
    """Test ImplementationStatusSchema validation."""

    def test_valid_complete_status(self) -> None:
        """Valid completion status with all required fields."""
        schema = ImplementationStatusSchema()
        data = {
            "status": "complete",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_valid_in_progress_status(self) -> None:
        """Valid in-progress status."""
        schema = ImplementationStatusSchema()
        data = {
            "status": "in_progress",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_optional_fields(self) -> None:
        """Valid status with optional fields."""
        schema = ImplementationStatusSchema()
        data = {
            "status": "complete",
            "timestamp": "2024-01-15T10:30:00Z",
            "summary": "Implemented feature X",
            "branch_name": "feature/x",
            "pr_url": "https://github.com/org/repo/pull/123",
            "commit_sha": "abc123def456",
            "worktree_path": "/path/to/worktree",
            "status_history": [{"status": "queued", "timestamp": "2024-01-15T10:00:00Z"}],
        }
        schema.validate(data)  # Should not raise

    def test_missing_required_field(self) -> None:
        """Missing required field raises ValueError."""
        schema = ImplementationStatusSchema()
        data = {
            "status": "complete",
            # Missing timestamp
        }
        with pytest.raises(ValueError, match="Missing required fields: timestamp"):
            schema.validate(data)

    def test_invalid_status_value(self) -> None:
        """Invalid status value raises ValueError."""
        schema = ImplementationStatusSchema()
        data = {
            "status": "invalid_status",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        with pytest.raises(ValueError, match="Invalid status 'invalid_status'"):
            schema.validate(data)

    def test_get_key(self) -> None:
        """get_key returns correct key."""
        schema = ImplementationStatusSchema()
        assert schema.get_key() == "erk-implementation-status"


class TestWorktreeCreationSchema:
    """Test WorktreeCreationSchema validation."""

    def test_valid_minimal(self) -> None:
        """Valid worktree creation with only required fields."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_optional_fields(self) -> None:
        """Valid worktree creation with optional fields."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
            "issue_number": 123,
            "plan_file": ".impl/plan.md",
        }
        schema.validate(data)  # Should not raise

    def test_invalid_issue_number_type(self) -> None:
        """issue_number must be integer."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
            "issue_number": "123",
        }
        with pytest.raises(ValueError, match="issue_number must be an integer"):
            schema.validate(data)

    def test_negative_issue_number(self) -> None:
        """issue_number must be positive."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
            "issue_number": -1,
        }
        with pytest.raises(ValueError, match="issue_number must be positive"):
            schema.validate(data)

    def test_empty_plan_file(self) -> None:
        """Empty plan_file raises ValueError."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
            "plan_file": "",
        }
        with pytest.raises(ValueError, match="plan_file must not be empty"):
            schema.validate(data)

    def test_unknown_fields(self) -> None:
        """Unknown fields raise ValueError."""
        schema = WorktreeCreationSchema()
        data = {
            "worktree_name": "feature-123",
            "branch_name": "feature/new-feature",
            "timestamp": "2024-01-15T10:30:00Z",
            "unknown_field": "value",
        }
        with pytest.raises(ValueError, match="Unknown fields: unknown_field"):
            schema.validate(data)


class TestPlanSchema:
    """Test PlanSchema validation."""

    def test_valid_minimal(self) -> None:
        """Valid plan with only required fields."""
        schema = PlanSchema()
        data = {
            "issue_number": 123,
            "worktree_name": "feature-123",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_plan_file(self) -> None:
        """Valid plan with optional plan_file."""
        schema = PlanSchema()
        data = {
            "issue_number": 123,
            "worktree_name": "feature-123",
            "timestamp": "2024-01-15T10:30:00Z",
            "plan_file": ".impl/plan.md",
        }
        schema.validate(data)  # Should not raise

    def test_invalid_issue_number_type(self) -> None:
        """issue_number must be integer."""
        schema = PlanSchema()
        data = {
            "issue_number": "123",
            "worktree_name": "feature-123",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        with pytest.raises(ValueError, match="issue_number must be an integer"):
            schema.validate(data)

    def test_zero_issue_number(self) -> None:
        """issue_number must be positive."""
        schema = PlanSchema()
        data = {
            "issue_number": 0,
            "worktree_name": "feature-123",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        with pytest.raises(ValueError, match="issue_number must be positive"):
            schema.validate(data)

    def test_empty_worktree_name(self) -> None:
        """Empty worktree_name raises ValueError."""
        schema = PlanSchema()
        data = {
            "issue_number": 123,
            "worktree_name": "",
            "timestamp": "2024-01-15T10:30:00Z",
        }
        with pytest.raises(ValueError, match="worktree_name must not be empty"):
            schema.validate(data)


class TestSubmissionQueuedSchema:
    """Test SubmissionQueuedSchema validation."""

    def test_valid_submission_queued(self) -> None:
        """Valid submission queued status."""
        schema = SubmissionQueuedSchema()
        data = {
            "status": "queued",
            "queued_at": "2024-01-15T10:30:00Z",
            "submitted_by": "john.doe",
            "issue_number": 123,
            "validation_results": {"issue_is_open": True, "has_erk_plan_label": True},
            "expected_workflow": "implement-plan.yml",
            "trigger_mechanism": "label-based-webhook",
        }
        schema.validate(data)  # Should not raise

    def test_invalid_status(self) -> None:
        """Status must be 'queued'."""
        schema = SubmissionQueuedSchema()
        data = {
            "status": "started",
            "queued_at": "2024-01-15T10:30:00Z",
            "submitted_by": "john.doe",
            "issue_number": 123,
            "validation_results": {},
            "expected_workflow": "implement-plan.yml",
            "trigger_mechanism": "label-based-webhook",
        }
        with pytest.raises(ValueError, match="Invalid status 'started'. Must be 'queued'"):
            schema.validate(data)

    def test_empty_queued_at(self) -> None:
        """Empty queued_at raises ValueError."""
        schema = SubmissionQueuedSchema()
        data = {
            "status": "queued",
            "queued_at": "",
            "submitted_by": "john.doe",
            "issue_number": 123,
            "validation_results": {},
            "expected_workflow": "implement-plan.yml",
            "trigger_mechanism": "label-based-webhook",
        }
        with pytest.raises(ValueError, match="queued_at must not be empty"):
            schema.validate(data)

    def test_non_dict_validation_results(self) -> None:
        """validation_results must be dict."""
        schema = SubmissionQueuedSchema()
        data = {
            "status": "queued",
            "queued_at": "2024-01-15T10:30:00Z",
            "submitted_by": "john.doe",
            "issue_number": 123,
            "validation_results": "not a dict",
            "expected_workflow": "implement-plan.yml",
            "trigger_mechanism": "label-based-webhook",
        }
        with pytest.raises(ValueError, match="validation_results must be a dict"):
            schema.validate(data)


class TestWorkflowStartedSchema:
    """Test WorkflowStartedSchema validation."""

    def test_valid_minimal(self) -> None:
        """Valid workflow started with only required fields."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "started",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "123456789",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_optional_fields(self) -> None:
        """Valid workflow started with optional fields."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "started",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "123456789",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
            "branch_name": "feature/new-feature",
            "worktree_path": "/path/to/worktree",
        }
        schema.validate(data)  # Should not raise

    def test_invalid_status(self) -> None:
        """Status must be 'started'."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "completed",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "123456789",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
        }
        with pytest.raises(ValueError, match="Invalid status 'completed'. Must be 'started'"):
            schema.validate(data)

    def test_empty_workflow_run_id(self) -> None:
        """Empty workflow_run_id raises ValueError."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "started",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
        }
        with pytest.raises(ValueError, match="workflow_run_id must not be empty"):
            schema.validate(data)

    def test_empty_branch_name(self) -> None:
        """Empty branch_name raises ValueError."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "started",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "123456789",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
            "branch_name": "",
        }
        with pytest.raises(ValueError, match="branch_name must not be empty"):
            schema.validate(data)

    def test_unknown_fields(self) -> None:
        """Unknown fields raise ValueError."""
        schema = WorkflowStartedSchema()
        data = {
            "status": "started",
            "started_at": "2024-01-15T10:30:00Z",
            "workflow_run_id": "123456789",
            "workflow_run_url": "https://github.com/org/repo/actions/runs/123456789",
            "issue_number": 123,
            "unknown_field": "value",
        }
        with pytest.raises(ValueError, match="Unknown fields: unknown_field"):
            schema.validate(data)


class TestPlanHeaderSchema:
    """Test PlanHeaderSchema validation."""

    def test_valid_without_worktree_name(self) -> None:
        """Valid plan-header without worktree_name (new issues before worktree creation)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_worktree_name(self) -> None:
        """Valid plan-header with worktree_name (after worktree creation)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "worktree_name": "my-feature-25-11-28",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_all_optional_fields(self) -> None:
        """Valid plan-header with all optional fields."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "worktree_name": "my-feature-25-11-28",
            "last_dispatched_run_id": "123456789",
            "last_dispatched_at": "2024-01-15T11:00:00Z",
            "last_local_impl_at": "2024-01-15T12:00:00Z",
            "last_remote_impl_at": "2024-01-15T13:00:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_valid_with_last_remote_impl_at(self) -> None:
        """Valid plan-header with last_remote_impl_at field."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_at": "2024-01-15T14:00:00Z",
        }
        schema.validate(data)  # Should not raise

    def test_null_last_remote_impl_at_is_valid(self) -> None:
        """Null last_remote_impl_at is valid (not set yet)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_at": None,
        }
        schema.validate(data)  # Should not raise

    def test_non_string_last_remote_impl_at_raises(self) -> None:
        """Non-string last_remote_impl_at raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_at": 12345,
        }
        with pytest.raises(ValueError, match="last_remote_impl_at must be a string or null"):
            schema.validate(data)

    def test_valid_with_remote_impl_run_id(self) -> None:
        """Valid plan-header with last_remote_impl_run_id field."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_run_id": "12345678",
        }
        schema.validate(data)  # Should not raise

    def test_null_remote_impl_run_id_is_valid(self) -> None:
        """Null last_remote_impl_run_id is valid (not set yet)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_run_id": None,
        }
        schema.validate(data)  # Should not raise

    def test_non_string_remote_impl_run_id_raises(self) -> None:
        """Non-string last_remote_impl_run_id raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_run_id": 12345,
        }
        with pytest.raises(ValueError, match="last_remote_impl_run_id must be a string or null"):
            schema.validate(data)

    def test_valid_with_remote_impl_session_id(self) -> None:
        """Valid plan-header with last_remote_impl_session_id field."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_session_id": "abc-123-session",
        }
        schema.validate(data)  # Should not raise

    def test_null_remote_impl_session_id_is_valid(self) -> None:
        """Null last_remote_impl_session_id is valid (not set yet)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_session_id": None,
        }
        schema.validate(data)  # Should not raise

    def test_non_string_remote_impl_session_id_raises(self) -> None:
        """Non-string last_remote_impl_session_id raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "last_remote_impl_session_id": 12345,
        }
        with pytest.raises(
            ValueError, match="last_remote_impl_session_id must be a string or null"
        ):
            schema.validate(data)

    def test_missing_required_field(self) -> None:
        """Missing required field raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            # Missing created_by
        }
        with pytest.raises(ValueError, match="Missing required fields: created_by"):
            schema.validate(data)

    def test_invalid_schema_version(self) -> None:
        """Invalid schema_version raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "1",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
        }
        with pytest.raises(ValueError, match="Invalid schema_version '1'. Must be '2'"):
            schema.validate(data)

    def test_empty_worktree_name_raises(self) -> None:
        """Empty worktree_name raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "worktree_name": "",
        }
        with pytest.raises(ValueError, match="worktree_name must not be empty when provided"):
            schema.validate(data)

    def test_non_string_worktree_name_raises(self) -> None:
        """Non-string worktree_name raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "worktree_name": 123,
        }
        with pytest.raises(ValueError, match="worktree_name must be a string or null"):
            schema.validate(data)

    def test_null_worktree_name_is_valid(self) -> None:
        """Null worktree_name is valid (not set yet)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "worktree_name": None,
        }
        schema.validate(data)  # Should not raise

    def test_unknown_fields_raises(self) -> None:
        """Unknown fields raise ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "unknown_field": "value",
        }
        with pytest.raises(ValueError, match="Unknown fields: unknown_field"):
            schema.validate(data)

    def test_valid_with_plan_comment_id(self) -> None:
        """Valid plan-header with plan_comment_id field."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "plan_comment_id": 12345678,
        }
        schema.validate(data)  # Should not raise

    def test_null_plan_comment_id_is_valid(self) -> None:
        """Null plan_comment_id is valid (not set yet)."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "plan_comment_id": None,
        }
        schema.validate(data)  # Should not raise

    def test_non_integer_plan_comment_id_raises(self) -> None:
        """Non-integer plan_comment_id raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "plan_comment_id": "12345678",
        }
        with pytest.raises(ValueError, match="plan_comment_id must be an integer or null"):
            schema.validate(data)

    def test_zero_plan_comment_id_raises(self) -> None:
        """Zero plan_comment_id raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "plan_comment_id": 0,
        }
        with pytest.raises(ValueError, match="plan_comment_id must be positive when provided"):
            schema.validate(data)

    def test_negative_plan_comment_id_raises(self) -> None:
        """Negative plan_comment_id raises ValueError."""
        schema = PlanHeaderSchema()
        data = {
            "schema_version": "2",
            "created_at": "2024-01-15T10:30:00Z",
            "created_by": "testuser",
            "plan_comment_id": -1,
        }
        with pytest.raises(ValueError, match="plan_comment_id must be positive when provided"):
            schema.validate(data)

    def test_get_key(self) -> None:
        """get_key returns correct key."""
        schema = PlanHeaderSchema()
        assert schema.get_key() == "plan-header"


class TestPlanHeaderFieldConstants:
    """Test plan-header field name constants (Literal types)."""

    def test_required_field_values(self) -> None:
        """Required field constants have correct string values."""
        assert SCHEMA_VERSION == "schema_version"
        assert CREATED_AT == "created_at"
        assert CREATED_BY == "created_by"

    def test_optional_field_values(self) -> None:
        """Optional field constants have correct string values."""
        assert WORKTREE_NAME == "worktree_name"
        assert PLAN_COMMENT_ID == "plan_comment_id"
        assert LAST_DISPATCHED_RUN_ID == "last_dispatched_run_id"
        assert LAST_DISPATCHED_NODE_ID == "last_dispatched_node_id"
        assert LAST_DISPATCHED_AT == "last_dispatched_at"
        assert LAST_LOCAL_IMPL_AT == "last_local_impl_at"
        assert LAST_LOCAL_IMPL_EVENT == "last_local_impl_event"
        assert LAST_LOCAL_IMPL_SESSION == "last_local_impl_session"
        assert LAST_LOCAL_IMPL_USER == "last_local_impl_user"
        assert LAST_REMOTE_IMPL_AT == "last_remote_impl_at"
        assert LAST_REMOTE_IMPL_RUN_ID == "last_remote_impl_run_id"
        assert LAST_REMOTE_IMPL_SESSION_ID == "last_remote_impl_session_id"
        assert SOURCE_REPO == "source_repo"
        assert OBJECTIVE_ISSUE == "objective_issue"
        assert CREATED_FROM_SESSION == "created_from_session"
        assert LAST_LEARN_SESSION == "last_learn_session"
        assert LAST_LEARN_AT == "last_learn_at"

    def test_schema_uses_field_constants(self) -> None:
        """PlanHeaderSchema validation uses field constants consistently.

        This test ensures the schema validate() method uses the module-level
        field constants for field access, not hardcoded strings.
        """
        schema = PlanHeaderSchema()

        # Create valid data using field constants
        valid_data = {
            SCHEMA_VERSION: "2",
            CREATED_AT: "2024-01-15T10:30:00Z",
            CREATED_BY: "testuser",
        }
        schema.validate(valid_data)  # Should not raise

        # Verify optional fields are also recognized
        valid_data_with_optional = {
            SCHEMA_VERSION: "2",
            CREATED_AT: "2024-01-15T10:30:00Z",
            CREATED_BY: "testuser",
            WORKTREE_NAME: "my-worktree",
            LAST_LOCAL_IMPL_AT: "2024-01-15T12:00:00Z",
            LAST_LOCAL_IMPL_EVENT: "ended",
        }
        schema.validate(valid_data_with_optional)  # Should not raise
