"""
Tests for data models

Organized by model:
- Note model tests
- Session model tests
- Token model tests
- AuthState model tests
"""

import pytest
from datetime import datetime, timedelta
from pathlib import Path
from starpunk.models import Note, Session, Token, AuthState
from starpunk.utils import calculate_content_hash


class TestNoteModel:
    """Test Note model"""

    def test_from_row_with_dict(self, tmp_path):
        """Test creating Note from dictionary row"""
        row = {
            "id": 1,
            "slug": "test-note",
            "file_path": "notes/2024/11/test-note.md",
            "published": 1,
            "created_at": datetime(2024, 11, 18, 14, 30),
            "updated_at": datetime(2024, 11, 18, 14, 30),
            "content_hash": "abc123",
        }
        note = Note.from_row(row, tmp_path)
        assert note.id == 1
        assert note.slug == "test-note"
        assert note.file_path == "notes/2024/11/test-note.md"
        assert note.published is True
        assert note.content_hash == "abc123"

    def test_from_row_with_string_timestamps(self, tmp_path):
        """Test creating Note with ISO timestamp strings"""
        row = {
            "id": 1,
            "slug": "test-note",
            "file_path": "notes/2024/11/test-note.md",
            "published": True,
            "created_at": "2024-11-18T14:30:00Z",
            "updated_at": "2024-11-18T14:30:00Z",
            "content_hash": None,
        }
        note = Note.from_row(row, tmp_path)
        # Check year, month, day, hour, minute (ignore timezone)
        assert note.created_at.year == 2024
        assert note.created_at.month == 11
        assert note.created_at.day == 18
        assert note.created_at.hour == 14
        assert note.created_at.minute == 30

    def test_content_lazy_loading(self, tmp_path):
        """Test content is lazy-loaded from file"""
        # Create test note file
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("# Test Note\n\nContent here.")

        # Create note instance
        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        # Content should be loaded on first access
        content = note.content
        assert "# Test Note" in content
        assert "Content here." in content

        # Second access should return cached value
        content2 = note.content
        assert content2 == content

    def test_content_file_not_found(self, tmp_path):
        """Test content raises FileNotFoundError if file missing"""
        note = Note(
            id=1,
            slug="missing",
            file_path="notes/2024/11/missing.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        with pytest.raises(FileNotFoundError):
            _ = note.content

    def test_html_rendering(self, tmp_path):
        """Test HTML rendering with caching"""
        # Create test note
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("# Heading\n\nParagraph here.")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        html = note.html
        # Check for common markdown-rendered elements
        assert "<h1" in html  # Heading
        assert "Paragraph here." in html

    def test_html_caching(self, tmp_path):
        """Test HTML is cached after first render"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("Content")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        html1 = note.html
        html2 = note.html
        assert html1 == html2

    def test_title_extraction_from_heading(self, tmp_path):
        """Test title extraction from markdown heading"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("# My Note Title\n\nContent.")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        assert note.title == "My Note Title"

    def test_title_extraction_from_first_line(self, tmp_path):
        """Test title extraction from first line without heading"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("Just a note without heading\n\nMore content.")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        assert note.title == "Just a note without heading"

    def test_title_fallback_to_slug(self):
        """Test title falls back to slug if no content"""
        note = Note(
            id=1,
            slug="my-test-note",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=Path("/nonexistent"),
        )

        # Should fall back to slug (file doesn't exist)
        assert note.title == "my-test-note"

    def test_excerpt_generation(self, tmp_path):
        """Test excerpt generation"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        content = "# Title\n\n" + ("This is a test. " * 50)  # Long content
        note_file.write_text(content)

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        excerpt = note.excerpt
        assert len(excerpt) <= 203  # EXCERPT_LENGTH + "..."
        assert excerpt.endswith("...")

    def test_excerpt_short_content(self, tmp_path):
        """Test excerpt with short content (no ellipsis)"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("# Title\n\nShort content.")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        excerpt = note.excerpt
        assert "Short content." in excerpt
        assert not excerpt.endswith("...")

    def test_excerpt_fallback_to_slug(self):
        """Test excerpt falls back to slug if file missing"""
        note = Note(
            id=1,
            slug="my-note",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=Path("/nonexistent"),
        )

        assert note.excerpt == "my-note"

    def test_permalink(self):
        """Test permalink generation"""
        note = Note(
            id=1,
            slug="my-note",
            file_path="notes/2024/11/my-note.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=Path("data"),
        )

        assert note.permalink == "/note/my-note"

    def test_is_published_property(self):
        """Test is_published property"""
        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=Path("data"),
        )

        assert note.is_published is True
        assert note.is_published == note.published

    def test_to_dict_basic(self):
        """Test serialization to dictionary"""
        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime(2024, 11, 18, 14, 30),
            updated_at=datetime(2024, 11, 18, 14, 30),
            _data_dir=Path("data"),
        )

        data = note.to_dict()
        assert data["id"] == 1
        assert data["slug"] == "test"
        assert data["published"] is True
        assert data["permalink"] == "/note/test"
        assert "content" not in data  # Not included by default
        assert "html" not in data  # Not included by default

    def test_to_dict_with_content(self, tmp_path):
        """Test serialization includes content when requested"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("Test content")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        data = note.to_dict(include_content=True)
        assert "content" in data
        assert data["content"] == "Test content"

    def test_to_dict_with_html(self, tmp_path):
        """Test serialization includes HTML when requested"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("# Test")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            _data_dir=tmp_path,
        )

        data = note.to_dict(include_html=True)
        assert "html" in data
        assert "<h1" in data["html"]

    def test_verify_integrity_success(self, tmp_path):
        """Test content integrity verification succeeds"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        content = "Test content"
        note_file.write_text(content)

        # Calculate correct hash
        content_hash = calculate_content_hash(content)

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            content_hash=content_hash,
            _data_dir=tmp_path,
        )

        # Should verify successfully
        assert note.verify_integrity() is True

    def test_verify_integrity_failure(self, tmp_path):
        """Test content integrity verification fails when modified"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        content = "Test content"
        note_file.write_text(content)

        # Calculate hash of original content
        content_hash = calculate_content_hash(content)

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            content_hash=content_hash,
            _data_dir=tmp_path,
        )

        # Modify file
        note_file.write_text("Modified content")

        # Should fail verification
        assert note.verify_integrity() is False

    def test_verify_integrity_no_hash(self, tmp_path):
        """Test integrity verification fails if no hash stored"""
        note_file = tmp_path / "notes" / "2024" / "11" / "test.md"
        note_file.parent.mkdir(parents=True)
        note_file.write_text("Content")

        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            content_hash=None,
            _data_dir=tmp_path,
        )

        # Should return False (no hash to verify against)
        assert note.verify_integrity() is False

    def test_verify_integrity_file_not_found(self):
        """Test integrity verification fails if file missing"""
        note = Note(
            id=1,
            slug="test",
            file_path="notes/2024/11/test.md",
            published=True,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow(),
            content_hash="abc123",
            _data_dir=Path("/nonexistent"),
        )

        assert note.verify_integrity() is False


class TestSessionModel:
    """Test Session model"""

    def test_from_row_with_dict(self):
        """Test creating Session from dictionary row"""
        row = {
            "id": 1,
            "session_token": "abc123",
            "me": "https://alice.example.com",
            "created_at": datetime(2024, 11, 18, 14, 30),
            "expires_at": datetime(2024, 12, 18, 14, 30),
            "last_used_at": None,
        }
        session = Session.from_row(row)
        assert session.id == 1
        assert session.session_token == "abc123"
        assert session.me == "https://alice.example.com"

    def test_from_row_with_string_timestamps(self):
        """Test creating Session with ISO timestamp strings"""
        row = {
            "id": 1,
            "session_token": "abc123",
            "me": "https://alice.example.com",
            "created_at": "2024-11-18T14:30:00Z",
            "expires_at": "2024-12-18T14:30:00Z",
            "last_used_at": "2024-11-18T15:00:00Z",
        }
        session = Session.from_row(row)
        # Check timestamps were parsed correctly (ignore timezone)
        assert session.created_at.year == 2024
        assert session.created_at.month == 11
        assert session.expires_at.month == 12
        assert session.last_used_at.hour == 15

    def test_is_expired_false(self):
        """Test is_expired returns False for active session"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        assert session.is_expired is False
        assert session.is_active is True

    def test_is_expired_true(self):
        """Test is_expired returns True for expired session"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow() - timedelta(days=31),
            expires_at=datetime.utcnow() - timedelta(days=1),
        )
        assert session.is_expired is True
        assert session.is_active is False

    def test_age(self):
        """Test age calculation"""
        created = datetime.utcnow() - timedelta(hours=2)
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=created,
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        age = session.age
        # Age should be at least 2 hours (account for test execution time)
        assert age.total_seconds() >= 7200

    def test_time_until_expiry(self):
        """Test time until expiry calculation"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        time_left = session.time_until_expiry
        # Should be approximately 30 days
        assert time_left.days >= 29

    def test_time_until_expiry_negative(self):
        """Test time until expiry is negative for expired session"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow() - timedelta(days=31),
            expires_at=datetime.utcnow() - timedelta(days=1),
        )
        time_left = session.time_until_expiry
        assert time_left.total_seconds() < 0

    def test_is_valid_active_session(self):
        """Test validation succeeds for active session"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        assert session.is_valid() is True

    def test_is_valid_expired_session(self):
        """Test validation fails for expired session"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow() - timedelta(days=31),
            expires_at=datetime.utcnow() - timedelta(days=1),
        )
        assert session.is_valid() is False

    def test_is_valid_empty_token(self):
        """Test validation fails for empty token"""
        session = Session(
            id=1,
            session_token="",
            me="https://alice.example.com",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        assert session.is_valid() is False

    def test_is_valid_empty_me(self):
        """Test validation fails for empty me URL"""
        session = Session(
            id=1,
            session_token="abc123",
            me="",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
        )
        assert session.is_valid() is False

    def test_with_updated_last_used(self):
        """Test creating session with updated timestamp"""
        original = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(days=30),
            last_used_at=None,
        )

        updated = original.with_updated_last_used()
        assert updated.last_used_at is not None
        assert updated.session_token == original.session_token
        assert updated.id == original.id

    def test_to_dict(self):
        """Test serialization to dictionary"""
        session = Session(
            id=1,
            session_token="abc123",
            me="https://alice.example.com",
            created_at=datetime(2024, 11, 18, 14, 30),
            expires_at=datetime(2024, 12, 18, 14, 30),
        )
        data = session.to_dict()
        assert data["id"] == 1
        assert data["me"] == "https://alice.example.com"
        assert "session_token" not in data  # Excluded for security
        assert "is_active" in data


class TestTokenModel:
    """Test Token model"""

    def test_from_row_with_dict(self):
        """Test creating Token from dictionary row"""
        row = {
            "token": "xyz789",
            "me": "https://alice.example.com",
            "client_id": "https://quill.p3k.io",
            "scope": "create update",
            "created_at": datetime(2024, 11, 18, 14, 30),
            "expires_at": None,
        }
        token = Token.from_row(row)
        assert token.token == "xyz789"
        assert token.me == "https://alice.example.com"
        assert token.scope == "create update"

    def test_from_row_with_string_timestamps(self):
        """Test creating Token with ISO timestamp strings"""
        row = {
            "token": "xyz789",
            "me": "https://alice.example.com",
            "client_id": None,
            "scope": "create",
            "created_at": "2024-11-18T14:30:00Z",
            "expires_at": "2025-02-18T14:30:00Z",
        }
        token = Token.from_row(row)
        # Check timestamps were parsed correctly (ignore timezone)
        assert token.created_at.year == 2024
        assert token.created_at.month == 11
        assert token.expires_at.year == 2025
        assert token.expires_at.month == 2

    def test_scopes_property(self):
        """Test scope parsing"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            scope="create update delete",
        )
        assert token.scopes == ["create", "update", "delete"]

    def test_scopes_empty(self):
        """Test empty scope"""
        token = Token(token="xyz789", me="https://alice.example.com", scope=None)
        assert token.scopes == []

    def test_scopes_whitespace(self):
        """Test scope with multiple spaces"""
        token = Token(
            token="xyz789", me="https://alice.example.com", scope="create  update"
        )
        scopes = token.scopes
        assert "create" in scopes
        assert "update" in scopes

    def test_has_scope_true(self):
        """Test scope checking returns True"""
        token = Token(
            token="xyz789", me="https://alice.example.com", scope="create update"
        )
        assert token.has_scope("create") is True
        assert token.has_scope("update") is True

    def test_has_scope_false(self):
        """Test scope checking returns False"""
        token = Token(
            token="xyz789", me="https://alice.example.com", scope="create update"
        )
        assert token.has_scope("delete") is False

    def test_has_scope_empty(self):
        """Test scope checking with no scopes"""
        token = Token(token="xyz789", me="https://alice.example.com", scope=None)
        assert token.has_scope("create") is False

    def test_is_expired_never_expires(self):
        """Test token with no expiry"""
        token = Token(token="xyz789", me="https://alice.example.com", expires_at=None)
        assert token.is_expired is False
        assert token.is_active is True

    def test_is_expired_with_future_expiry(self):
        """Test token with future expiry"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            expires_at=datetime.utcnow() + timedelta(days=90),
        )
        assert token.is_expired is False
        assert token.is_active is True

    def test_is_expired_with_past_expiry(self):
        """Test token with past expiry"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            expires_at=datetime.utcnow() - timedelta(days=1),
        )
        assert token.is_expired is True
        assert token.is_active is False

    def test_is_valid_active_token(self):
        """Test validation succeeds for active token"""
        token = Token(token="xyz789", me="https://alice.example.com", scope="create")
        assert token.is_valid() is True

    def test_is_valid_expired_token(self):
        """Test validation fails for expired token"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            expires_at=datetime.utcnow() - timedelta(days=1),
        )
        assert token.is_valid() is False

    def test_is_valid_empty_token(self):
        """Test validation fails for empty token"""
        token = Token(token="", me="https://alice.example.com")
        assert token.is_valid() is False

    def test_is_valid_with_required_scope_success(self):
        """Test validation with required scope succeeds"""
        token = Token(
            token="xyz789", me="https://alice.example.com", scope="create update"
        )
        assert token.is_valid(required_scope="create") is True

    def test_is_valid_with_required_scope_failure(self):
        """Test validation with required scope fails"""
        token = Token(
            token="xyz789", me="https://alice.example.com", scope="create update"
        )
        assert token.is_valid(required_scope="delete") is False

    def test_to_dict(self):
        """Test serialization to dictionary"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            client_id="https://quill.p3k.io",
            scope="create update",
            created_at=datetime(2024, 11, 18, 14, 30),
        )
        data = token.to_dict()
        assert data["me"] == "https://alice.example.com"
        assert data["client_id"] == "https://quill.p3k.io"
        assert data["scope"] == "create update"
        assert "token" not in data  # Excluded for security
        assert "is_active" in data

    def test_to_dict_with_expires_at(self):
        """Test serialization includes expires_at if set"""
        token = Token(
            token="xyz789",
            me="https://alice.example.com",
            created_at=datetime(2024, 11, 18, 14, 30),
            expires_at=datetime(2025, 2, 18, 14, 30),
        )
        data = token.to_dict()
        assert "expires_at" in data


class TestAuthStateModel:
    """Test AuthState model"""

    def test_from_row_with_dict(self):
        """Test creating AuthState from dictionary row"""
        row = {
            "state": "random123",
            "created_at": datetime(2024, 11, 18, 14, 30),
            "expires_at": datetime(2024, 11, 18, 14, 35),
        }
        auth_state = AuthState.from_row(row)
        assert auth_state.state == "random123"

    def test_from_row_with_string_timestamps(self):
        """Test creating AuthState with ISO timestamp strings"""
        row = {
            "state": "random123",
            "created_at": "2024-11-18T14:30:00Z",
            "expires_at": "2024-11-18T14:35:00Z",
        }
        auth_state = AuthState.from_row(row)
        # Check timestamps were parsed correctly (ignore timezone)
        assert auth_state.created_at.year == 2024
        assert auth_state.created_at.minute == 30
        assert auth_state.expires_at.minute == 35

    def test_is_expired_false(self):
        """Test is_expired returns False for active state"""
        auth_state = AuthState(
            state="random123",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(minutes=5),
        )
        assert auth_state.is_expired is False
        assert auth_state.is_active is True

    def test_is_expired_true(self):
        """Test is_expired returns True for expired state"""
        auth_state = AuthState(
            state="random123",
            created_at=datetime.utcnow() - timedelta(minutes=10),
            expires_at=datetime.utcnow() - timedelta(minutes=5),
        )
        assert auth_state.is_expired is True
        assert auth_state.is_active is False

    def test_age(self):
        """Test age calculation"""
        created = datetime.utcnow() - timedelta(minutes=2)
        auth_state = AuthState(
            state="random123",
            created_at=created,
            expires_at=datetime.utcnow() + timedelta(minutes=3),
        )
        age = auth_state.age
        # Age should be at least 2 minutes
        assert age.total_seconds() >= 120

    def test_is_valid_active_state(self):
        """Test validation succeeds for active state"""
        auth_state = AuthState(
            state="random123",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(minutes=5),
        )
        assert auth_state.is_valid() is True

    def test_is_valid_expired_state(self):
        """Test validation fails for expired state"""
        auth_state = AuthState(
            state="random123",
            created_at=datetime.utcnow() - timedelta(minutes=10),
            expires_at=datetime.utcnow() - timedelta(minutes=5),
        )
        assert auth_state.is_valid() is False

    def test_is_valid_empty_state(self):
        """Test validation fails for empty state"""
        auth_state = AuthState(
            state="",
            created_at=datetime.utcnow(),
            expires_at=datetime.utcnow() + timedelta(minutes=5),
        )
        assert auth_state.is_valid() is False

    def test_to_dict(self):
        """Test serialization to dictionary"""
        auth_state = AuthState(
            state="random123",
            created_at=datetime(2024, 11, 18, 14, 30),
            expires_at=datetime(2024, 11, 18, 14, 35),
        )
        data = auth_state.to_dict()
        assert "created_at" in data
        assert "expires_at" in data
        assert "is_active" in data
        assert "state" not in data  # State value not included in dict
