"""Integration tests for knowledge entry validation — staleness and duplicate detection.

Tests the end-to-end flow of:
- KnowledgeValidator.validate_stale() with boundary conditions
- dedup_observations(), should_deduplicate(), increment_observation()
- Combined validation + dedup workflows
"""
import json
import sqlite3
import uuid
from datetime import datetime, timedelta, timezone

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.dedup import (
    dedup_observations,
    increment_observation,
    should_deduplicate,
)
from greymatter_core.knowledge import KnowledgeStore
from greymatter_core.validate import KnowledgeValidator, ValidationReport


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def db():
    """In-memory SQLite database with full schema."""
    return SQLiteDatabase(":memory:")


@pytest.fixture
def knowledge(db):
    return KnowledgeStore(db)


@pytest.fixture
def validator(db):
    return KnowledgeValidator(db)


@pytest.fixture
def raw_conn(db):
    """Raw sqlite3 connection from the Database — needed by dedup module."""
    return db._conn


def _insert_knowledge_entry(
    db,
    *,
    entry_id: str | None = None,
    entry_type: str = "observation",
    title: str = "Test entry",
    content: str = "Test content",
    confidence: float = 0.5,
    created_at: str = "2024-01-01 00:00:00",
    updated_at: str | None = None,
    pruned_at: str | None = None,
) -> str:
    """Insert a knowledge entry with explicit timestamps for testing."""
    eid = entry_id or f"kn-{uuid.uuid4().hex[:8]}"
    db.execute(
        """INSERT INTO knowledge_entries
           (id, entry_type, title, content, confidence, created_at, updated_at, pruned_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
        (eid, entry_type, title, content, confidence, created_at, updated_at, pruned_at),
    )
    return eid


def _insert_observation(
    conn: sqlite3.Connection,
    *,
    obs_id: str | None = None,
    session_id: str = "sess-test",
    title: str = "Test observation",
    content: str = '{"detail": "test"}',
    created_at: str | None = None,
    updated_at: str | None = None,
    pruned: int = 0,
) -> str:
    """Insert an observation row for dedup testing."""
    oid = obs_id or f"obs-{uuid.uuid4().hex[:8]}"
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
    conn.execute(
        """INSERT INTO observations
           (id, session_id, trigger, category, title, content, created_at, updated_at, pruned)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (
            oid,
            session_id,
            "test",
            "test",
            title,
            content,
            created_at or now,
            updated_at or now,
            pruned,
        ),
    )
    conn.commit()
    return oid


# ===========================================================================
# Staleness validation — edge cases
# ===========================================================================


class TestStalenessValidation:
    """Integration tests for KnowledgeValidator.validate_stale()."""

    def test_confidence_exactly_0_5_is_included(self, db, validator):
        """Entries with confidence == 0.5 (the boundary) should be checked."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-boundary",
            confidence=0.5,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        report = validator.validate_stale()
        assert report.stale >= 1
        assert any("kn-boundary" in i.entry_id for i in report.issues)

    def test_confidence_0_51_is_excluded(self, db, validator):
        """Entries with confidence > 0.5 should NOT be flagged as stale."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-above",
            confidence=0.51,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        report = validator.validate_stale()
        assert not any("kn-above" in i.entry_id for i in report.issues)

    def test_pruned_entries_excluded(self, db, validator):
        """Pruned entries should never appear in staleness results."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-pruned",
            confidence=0.2,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
            pruned_at="2025-01-01 00:00:00",
        )
        report = validator.validate_stale()
        assert not any("kn-pruned" in i.entry_id for i in report.issues)

    def test_custom_days_parameter(self, db, validator):
        """Custom days threshold should change what counts as stale."""
        # Entry is 10 days old
        ten_days_ago = (
            datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=10)
        ).strftime("%Y-%m-%d %H:%M:%S")
        _insert_knowledge_entry(
            db,
            entry_id="kn-custom",
            confidence=0.3,
            created_at=ten_days_ago,
            updated_at=ten_days_ago,
        )
        # Default 30 days — should NOT be stale
        report_30 = validator.validate_stale(days=30)
        assert not any("kn-custom" in i.entry_id for i in report_30.issues)

        # 5 days threshold — SHOULD be stale
        report_5 = validator.validate_stale(days=5)
        assert any("kn-custom" in i.entry_id for i in report_5.issues)

    def test_null_updated_at_uses_created_at(self, db, validator):
        """When updated_at is NULL, COALESCE falls back to created_at."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-nullup",
            confidence=0.3,
            created_at="2024-01-01 00:00:00",
            updated_at=None,
        )
        report = validator.validate_stale()
        assert any("kn-nullup" in i.entry_id for i in report.issues)

    def test_recently_updated_old_entry_not_stale(self, db, validator):
        """An old entry that was recently updated should not be stale."""
        recent = (
            datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=1)
        ).strftime("%Y-%m-%d %H:%M:%S")
        _insert_knowledge_entry(
            db,
            entry_id="kn-refreshed",
            confidence=0.3,
            created_at="2024-01-01 00:00:00",
            updated_at=recent,
        )
        report = validator.validate_stale()
        assert not any("kn-refreshed" in i.entry_id for i in report.issues)

    def test_multiple_stale_entries(self, db, validator):
        """Multiple stale entries should all appear in the report."""
        for i in range(5):
            _insert_knowledge_entry(
                db,
                entry_id=f"kn-multi{i:02d}",
                confidence=0.2,
                created_at="2023-06-01 00:00:00",
                updated_at="2023-06-01 00:00:00",
            )
        report = validator.validate_stale()
        assert report.stale >= 5
        found_ids = {issue.entry_id for issue in report.issues}
        for i in range(5):
            assert f"kn-multi{i:02d}" in found_ids

    def test_stale_report_valid_count(self, db, validator):
        """Valid count = total low-confidence entries minus stale ones."""
        # One stale (old)
        _insert_knowledge_entry(
            db,
            entry_id="kn-old",
            confidence=0.4,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        # One not stale (recent, low confidence)
        recent = (
            datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=1)
        ).strftime("%Y-%m-%d %H:%M:%S")
        _insert_knowledge_entry(
            db,
            entry_id="kn-fresh",
            confidence=0.4,
            created_at=recent,
            updated_at=recent,
        )
        report = validator.validate_stale()
        assert report.stale == 1
        assert report.valid == 1

    def test_zero_confidence_entry_stale(self, db, validator):
        """Entries with confidence=0.0 should be flagged if old enough."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-zero",
            confidence=0.0,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        report = validator.validate_stale()
        assert any("kn-zero" in i.entry_id for i in report.issues)

    def test_stale_issue_contains_confidence(self, db, validator):
        """Stale issue description should include the confidence value."""
        _insert_knowledge_entry(
            db,
            entry_id="kn-detail",
            confidence=0.25,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        report = validator.validate_stale()
        issue = next(i for i in report.issues if i.entry_id == "kn-detail")
        assert "0.25" in issue.issue
        assert "Stale" in issue.issue

    def test_stale_limit_50(self, db, validator):
        """Staleness check is capped at 50 entries."""
        for i in range(60):
            _insert_knowledge_entry(
                db,
                entry_id=f"kn-bulk{i:03d}",
                confidence=0.1,
                created_at="2023-01-01 00:00:00",
                updated_at="2023-01-01 00:00:00",
            )
        report = validator.validate_stale()
        assert report.stale == 50  # LIMIT 50 in the SQL


# ===========================================================================
# Duplicate detection — dedup_observations
# ===========================================================================


class TestDedupObservations:
    """Integration tests for dedup_observations()."""

    def test_basic_merge(self, raw_conn):
        """Two observations with same title within window get merged."""
        now = datetime.now(timezone.utc)
        t1 = now.strftime("%Y-%m-%d %H:%M:%S")
        t2 = (now + timedelta(seconds=30)).strftime("%Y-%m-%d %H:%M:%S")

        _insert_observation(raw_conn, obs_id="obs-a1", title="Repeated", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-a2", title="Repeated", created_at=t2, updated_at=t2)

        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 1

        # Keeper should have dedup_count
        row = raw_conn.execute("SELECT content FROM observations WHERE id = 'obs-a1'").fetchone()
        content = json.loads(row[0])
        assert content["dedup_count"] == 2
        assert "obs-a2" in content["dedup_merged_ids"]

        # Duplicate should be deleted
        gone = raw_conn.execute("SELECT id FROM observations WHERE id = 'obs-a2'").fetchone()
        assert gone is None

    def test_no_duplicates_returns_zero(self, raw_conn):
        """When all titles are unique, nothing is merged."""
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        _insert_observation(raw_conn, obs_id="obs-u1", title="Alpha", created_at=now, updated_at=now)
        _insert_observation(raw_conn, obs_id="obs-u2", title="Beta", created_at=now, updated_at=now)

        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 0

    def test_outside_window_not_merged(self, raw_conn):
        """Same title but outside the time window should not merge."""
        t1 = "2025-01-01 00:00:00"
        t2 = "2025-01-01 00:10:00"  # 10 minutes apart

        _insert_observation(raw_conn, obs_id="obs-w1", title="Same", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-w2", title="Same", created_at=t2, updated_at=t2)

        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 0

    def test_dry_run_does_not_modify(self, raw_conn):
        """dry_run=True should report count but not delete anything."""
        now = datetime.now(timezone.utc)
        t1 = now.strftime("%Y-%m-%d %H:%M:%S")
        t2 = (now + timedelta(seconds=30)).strftime("%Y-%m-%d %H:%M:%S")

        _insert_observation(raw_conn, obs_id="obs-d1", title="Dup", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-d2", title="Dup", created_at=t2, updated_at=t2)

        deleted = dedup_observations(raw_conn, window_minutes=5, dry_run=True)
        assert deleted == 1

        # Both rows should still exist
        count = raw_conn.execute("SELECT COUNT(*) FROM observations").fetchone()[0]
        assert count == 2

    def test_multiple_groups_merged(self, raw_conn):
        """Multiple groups of duplicates should all be merged independently."""
        now = datetime.now(timezone.utc)
        t1 = now.strftime("%Y-%m-%d %H:%M:%S")
        t2 = (now + timedelta(seconds=30)).strftime("%Y-%m-%d %H:%M:%S")

        # Group 1: "Alpha" x2
        _insert_observation(raw_conn, obs_id="obs-g1a", title="Alpha", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-g1b", title="Alpha", created_at=t2, updated_at=t2)
        # Group 2: "Beta" x3
        _insert_observation(raw_conn, obs_id="obs-g2a", title="Beta", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-g2b", title="Beta", created_at=t2, updated_at=t2)
        t3 = (now + timedelta(seconds=60)).strftime("%Y-%m-%d %H:%M:%S")
        _insert_observation(raw_conn, obs_id="obs-g2c", title="Beta", created_at=t3, updated_at=t3)

        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 3  # 1 from Alpha + 2 from Beta

        # Keepers survive
        remaining = raw_conn.execute("SELECT id FROM observations ORDER BY id").fetchall()
        remaining_ids = {r[0] for r in remaining}
        assert "obs-g1a" in remaining_ids
        assert "obs-g2a" in remaining_ids

    def test_pruned_observations_excluded(self, raw_conn):
        """Pruned observations should not be considered for dedup."""
        now = datetime.now(timezone.utc)
        t1 = now.strftime("%Y-%m-%d %H:%M:%S")
        t2 = (now + timedelta(seconds=30)).strftime("%Y-%m-%d %H:%M:%S")

        _insert_observation(raw_conn, obs_id="obs-p1", title="Same", created_at=t1, updated_at=t1, pruned=0)
        _insert_observation(raw_conn, obs_id="obs-p2", title="Same", created_at=t2, updated_at=t2, pruned=1)

        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 0  # Pruned row is filtered out by WHERE pruned = 0

    def test_keeper_gets_latest_updated_at(self, raw_conn):
        """The keeper row should get the latest updated_at from the group."""
        t1 = "2025-01-01 00:00:00"
        t2 = "2025-01-01 00:01:00"

        _insert_observation(raw_conn, obs_id="obs-t1", title="Same", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-t2", title="Same", created_at=t2, updated_at=t2)

        dedup_observations(raw_conn, window_minutes=5)
        row = raw_conn.execute("SELECT updated_at FROM observations WHERE id = 'obs-t1'").fetchone()
        assert row[0] == t2

    def test_empty_table_returns_zero(self, raw_conn):
        """Empty observations table should return 0 without error."""
        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 0


# ===========================================================================
# Duplicate detection — should_deduplicate
# ===========================================================================


class TestShouldDeduplicate:
    """Integration tests for should_deduplicate()."""

    def test_finds_recent_duplicate(self, raw_conn):
        """Returns existing observation ID when a recent match exists.

        Note: should_deduplicate uses datetime.now(timezone.utc).isoformat() for the
        cutoff, which produces 'T'-separated timestamps.  SQLite string
        comparison means observations stored with space-separated timestamps
        sort *before* the 'T' cutoff.  We use isoformat() format here to
        match the comparison semantics.
        """
        now = datetime.now(timezone.utc).isoformat()
        _insert_observation(raw_conn, obs_id="obs-dup1", title="Recurring event", created_at=now, updated_at=now)

        result = should_deduplicate(raw_conn, "Recurring event", window_minutes=5)
        assert result == "obs-dup1"

    def test_no_match_returns_none(self, raw_conn):
        """Returns None when no matching title exists."""
        result = should_deduplicate(raw_conn, "Nonexistent title", window_minutes=5)
        assert result is None

    def test_old_entry_not_matched(self, raw_conn):
        """Entries outside the time window should not match.

        Uses isoformat() to match should_deduplicate's cutoff format.
        """
        old_time = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat()
        _insert_observation(raw_conn, obs_id="obs-old", title="Old event", created_at=old_time, updated_at=old_time)

        result = should_deduplicate(raw_conn, "Old event", window_minutes=5)
        assert result is None

    def test_pruned_entry_not_matched(self, raw_conn):
        """Pruned observations should not be returned as duplicates."""
        now = datetime.now(timezone.utc).isoformat()
        _insert_observation(raw_conn, obs_id="obs-pr", title="Pruned event", created_at=now, updated_at=now, pruned=1)

        result = should_deduplicate(raw_conn, "Pruned event", window_minutes=5)
        assert result is None

    def test_returns_most_recent_match(self, raw_conn):
        """When multiple matches exist, returns the most recent one."""
        now = datetime.now(timezone.utc)
        t1 = (now - timedelta(minutes=2)).isoformat()
        t2 = (now - timedelta(minutes=1)).isoformat()

        _insert_observation(raw_conn, obs_id="obs-older", title="Multi", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-newer", title="Multi", created_at=t2, updated_at=t2)

        result = should_deduplicate(raw_conn, "Multi", window_minutes=5)
        assert result == "obs-newer"


# ===========================================================================
# Duplicate detection — increment_observation
# ===========================================================================


class TestIncrementObservation:
    """Integration tests for increment_observation()."""

    def test_increments_count(self, raw_conn):
        """First increment should set dedup_count to 2."""
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        _insert_observation(
            raw_conn,
            obs_id="obs-inc1",
            title="Counter",
            content='{"detail": "v1"}',
            created_at=now,
            updated_at=now,
        )

        increment_observation(raw_conn, "obs-inc1", '{"detail": "v2"}')

        row = raw_conn.execute("SELECT content FROM observations WHERE id = 'obs-inc1'").fetchone()
        content = json.loads(row[0])
        assert content["dedup_count"] == 2
        assert content["detail"] == "v2"  # Content updated to latest

    def test_increments_multiple_times(self, raw_conn):
        """Multiple increments should accumulate correctly."""
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        _insert_observation(
            raw_conn,
            obs_id="obs-inc2",
            title="Multi-inc",
            content='{"detail": "v1"}',
            created_at=now,
            updated_at=now,
        )

        increment_observation(raw_conn, "obs-inc2", '{"detail": "v2"}')
        increment_observation(raw_conn, "obs-inc2", '{"detail": "v3"}')
        increment_observation(raw_conn, "obs-inc2", '{"detail": "v4"}')

        row = raw_conn.execute("SELECT content FROM observations WHERE id = 'obs-inc2'").fetchone()
        content = json.loads(row[0])
        assert content["dedup_count"] == 4
        assert content["detail"] == "v4"

    def test_nonexistent_id_is_noop(self, raw_conn):
        """Incrementing a nonexistent ID should not raise."""
        increment_observation(raw_conn, "obs-ghost", '{"detail": "nope"}')
        # No error, no rows affected

    def test_non_json_content_handled(self, raw_conn):
        """Observations with non-JSON content should still increment safely."""
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        _insert_observation(
            raw_conn,
            obs_id="obs-plain",
            title="Plain text",
            content="not json",
            created_at=now,
            updated_at=now,
        )

        increment_observation(raw_conn, "obs-plain", '{"new": "data"}')

        row = raw_conn.execute("SELECT content FROM observations WHERE id = 'obs-plain'").fetchone()
        content = json.loads(row[0])
        assert content["dedup_count"] == 2
        assert content["original"] == "not json"
        assert content["new"] == "data"


# ===========================================================================
# End-to-end integration: validation + dedup combined
# ===========================================================================


class TestEndToEnd:
    """End-to-end tests combining staleness validation with dedup."""

    def test_create_via_store_then_validate_stale(self, knowledge, validator):
        """Entries created via KnowledgeStore should be found by staleness check."""
        # Create a fresh entry — should NOT be stale
        entry = knowledge.create(
            entry_type="observation",
            title="Fresh observation",
            content="Just created",
        )
        report = validator.validate_stale()
        assert not any(entry["id"] in i.entry_id for i in report.issues)

    def test_validate_all_with_mixed_issues(self, db, knowledge, validator):
        """validate_all should detect both path issues and staleness together."""
        # Bad path entry
        knowledge.create(
            entry_type="reference",
            title="Missing file",
            content="Config at /nonexistent/foo/bar.yaml",
        )
        # Stale low-confidence entry
        _insert_knowledge_entry(
            db,
            entry_id="kn-stale-e2e",
            confidence=0.2,
            title="Stale fact",
            created_at="2023-06-01 00:00:00",
            updated_at="2023-06-01 00:00:00",
        )

        report = validator.validate_all()
        assert report.invalid >= 1
        assert report.stale >= 1
        assert len(report.issues) >= 2

        # to_dict should be consistent
        d = report.to_dict()
        assert d["total_checked"] == d["valid"] + d["invalid"] + d["stale"]

    def test_dedup_then_validate(self, db, raw_conn, validator):
        """Dedup should clean observations; staleness should still work on knowledge."""
        # Create duplicate observations
        now = datetime.now(timezone.utc)
        t1 = now.strftime("%Y-%m-%d %H:%M:%S")
        t2 = (now + timedelta(seconds=10)).strftime("%Y-%m-%d %H:%M:%S")

        _insert_observation(raw_conn, obs_id="obs-e2e-1", title="Same obs", created_at=t1, updated_at=t1)
        _insert_observation(raw_conn, obs_id="obs-e2e-2", title="Same obs", created_at=t2, updated_at=t2)

        # Create a stale knowledge entry
        _insert_knowledge_entry(
            db,
            entry_id="kn-e2e-stale",
            confidence=0.3,
            created_at="2023-01-01 00:00:00",
            updated_at="2023-01-01 00:00:00",
        )

        # Run dedup
        deleted = dedup_observations(raw_conn, window_minutes=5)
        assert deleted == 1

        # Validation should still find the stale knowledge entry
        report = validator.validate_stale()
        assert any("kn-e2e-stale" in i.entry_id for i in report.issues)

    def test_should_dedup_then_increment_flow(self, raw_conn):
        """Full flow: check for dup → find match → increment → verify."""
        now = datetime.now(timezone.utc).isoformat()
        _insert_observation(
            raw_conn,
            obs_id="obs-flow1",
            title="Recurring pattern",
            content='{"detail": "first"}',
            created_at=now,
            updated_at=now,
        )

        # Check for duplicate
        existing_id = should_deduplicate(raw_conn, "Recurring pattern", window_minutes=5)
        assert existing_id == "obs-flow1"

        # Increment instead of creating new
        increment_observation(raw_conn, existing_id, '{"detail": "second"}')

        # Verify state
        row = raw_conn.execute("SELECT content FROM observations WHERE id = 'obs-flow1'").fetchone()
        content = json.loads(row[0])
        assert content["dedup_count"] == 2
        assert content["detail"] == "second"

        # Only one row should exist
        count = raw_conn.execute(
            "SELECT COUNT(*) FROM observations WHERE title = 'Recurring pattern'"
        ).fetchone()[0]
        assert count == 1

    def test_weekly_hook_records_and_blocks_rerun(self, validator):
        """Weekly hook should run once, record, then block re-run."""
        assert validator.should_run_weekly() is True
        report = validator.run_weekly_if_due()
        assert report is not None
        assert isinstance(report, ValidationReport)

        # Immediately after, should not run
        assert validator.should_run_weekly() is False
        report2 = validator.run_weekly_if_due()
        assert report2 is None

    def test_report_merge_preserves_issues(self):
        """Merging reports should combine all issues from both."""
        from greymatter_core.validate import ValidationIssue

        r1 = ValidationReport(valid=1, invalid=1, stale=0, issues=[
            ValidationIssue("kn-1", "Title 1", "Path missing"),
        ])
        r2 = ValidationReport(valid=0, invalid=0, stale=2, issues=[
            ValidationIssue("kn-2", "Title 2", "Stale entry"),
            ValidationIssue("kn-3", "Title 3", "Stale entry"),
        ])
        r1.merge(r2)
        assert r1.valid == 1
        assert r1.invalid == 1
        assert r1.stale == 2
        assert len(r1.issues) == 3

    def test_validate_all_empty_db(self, validator):
        """validate_all on empty DB should return clean report with no errors."""
        report = validator.validate_all()
        assert report.valid == 0
        assert report.invalid == 0
        assert report.stale == 0
        assert report.issues == []
