# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

import asyncio
import json
import logging
from pathlib import Path


def _write_detected_entities(tmp_path, facet: str, day: str, rows: list[dict]) -> None:
    path = tmp_path / "facets" / facet / "entities" / f"{day}.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows),
        encoding="utf-8",
    )


def _activity_record(record_id: str = "meeting_090000_300") -> dict:
    return {
        "id": record_id,
        "activity": "meeting",
        "description": "Team sync",
        "segments": ["090000_300"],
        "created_at": 1,
    }


def _context(
    tmp_path: Path,
    *,
    facet: str = "work",
    day: str = "20260418",
    record_id: str = "meeting_090000_300",
    name: str = "conversation",
) -> dict:
    return {
        "facet": facet,
        "day": day,
        "name": name,
        "activity": {"id": record_id},
        "output_path": str(
            tmp_path / "facets" / facet / "activities" / day / record_id / "story.json"
        ),
    }


def _valid_result(**overrides) -> str:
    payload = {
        "body": "Aligned on launch work and assigned the follow-up.",
        "topics": ["launch", "follow-up"],
        "confidence": 0.82,
        "commitments": [
            {
                "owner": "Mina",
                "action": "send the revised deck",
                "counterparty": "Ravi",
                "when": "Friday morning",
                "context": "Mina committed to send the deck before the next investor call.",
            }
        ],
        "closures": [
            {
                "owner": "Ravi",
                "action": "intro email",
                "counterparty": "Mina",
                "resolution": "sent",
                "context": "Ravi confirmed the intro email already went out.",
            }
        ],
        "decisions": [
            {
                "owner": "Team",
                "action": "move the launch review to Tuesday",
                "counterparty": None,
                "context": "The group aligned on Tuesday after checking calendars.",
            }
        ],
        "relations": [],
    }
    payload.update(overrides)
    return json.dumps(payload)


def _load_record(facet: str, day: str):
    from solstone.think.activities import load_activity_records

    return load_activity_records(facet, day, include_hidden=True)[0]


def test_story_hook_parses_and_writes(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))

    append_activity_record("work", "20260418", _activity_record())

    returned = post_process(
        _valid_result(body="  Wrapped the launch prep and assigned follow-up.  "),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert returned == ""
    assert record["story"] == {
        "talent": "conversation",
        "body": "Wrapped the launch prep and assigned follow-up.",
        "topics": ["launch", "follow-up"],
        "confidence": 0.82,
    }
    assert record["commitments"][0]["owner"] == "Mina"
    assert record["closures"][0]["resolution"] == "sent"
    assert record["decisions"][0]["owner"] == "Team"
    assert record["relations"] == []
    assert record["edits"][-1]["actor"] == "story"
    assert record["edits"][-1]["fields"] == [
        "story",
        "commitments",
        "closures",
        "decisions",
        "relations",
    ]


def test_story_hook_empty_arrays(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(commitments=[], closures=[], decisions=[]),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert (
        record["story"]["body"] == "Aligned on launch work and assigned the follow-up."
    )
    assert record["commitments"] == []
    assert record["closures"] == []
    assert record["decisions"] == []
    assert record["relations"] == []


def test_story_hook_bad_resolution_skipped(tmp_path, monkeypatch, caplog):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            closures=[
                {
                    "owner": "Ravi",
                    "action": "intro email",
                    "counterparty": "Mina",
                    "resolution": "sent",
                    "context": "The intro email went out.",
                },
                {
                    "owner": "Ravi",
                    "action": "budget request",
                    "counterparty": "Finance",
                    "resolution": "approved",
                    "context": "This resolution is invalid for the schema.",
                },
            ]
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert [closure["action"] for closure in record["closures"]] == ["intro email"]
    assert "invalid resolution 'approved'" in caplog.text


def test_story_hook_missing_required_field_skipped(tmp_path, monkeypatch, caplog):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            commitments=[
                {
                    "owner": "Mina",
                    "action": "send the revised deck",
                    "counterparty": "Ravi",
                    "when": "Friday morning",
                    "context": "Valid commitment.",
                },
                {
                    "owner": "Mina",
                    "action": "book the room",
                    "when": "tomorrow",
                    "context": "Missing counterparty should skip.",
                },
            ],
            closures=[
                {
                    "owner": "Ravi",
                    "action": "intro email",
                    "counterparty": "Mina",
                    "resolution": "sent",
                    "context": "Valid closure.",
                },
                {
                    "action": "parking pass",
                    "counterparty": "Travel desk",
                    "resolution": "done",
                    "context": "Missing owner should skip.",
                },
            ],
            decisions=[
                {
                    "owner": "Team",
                    "action": "move the launch review to Tuesday",
                    "counterparty": None,
                    "context": "Valid decision.",
                },
                {
                    "owner": "Team",
                    "context": "Missing action should skip.",
                },
            ],
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert len(record["commitments"]) == 1
    assert len(record["closures"]) == 1
    assert len(record["decisions"]) == 1
    assert "missing required string field" in caplog.text


def test_story_hook_resolves_entities(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    _write_detected_entities(
        tmp_path,
        "work",
        "20260418",
        [
            {"id": "mina_lee", "type": "Person", "name": "Mina Lee", "aka": ["Mina"]},
            {"id": "ravi_shah", "type": "Person", "name": "Ravi Shah", "aka": ["Ravi"]},
        ],
    )
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            commitments=[
                {
                    "owner": "Mina",
                    "action": "send the revised deck",
                    "counterparty": "Ravi",
                    "when": "Friday morning",
                    "context": "Valid commitment.",
                },
                {
                    "owner": "Unknown Owner",
                    "action": "draft the note",
                    "counterparty": "Unknown Counterparty",
                    "when": "later",
                    "context": "Unmatched names should stay null.",
                },
            ],
            closures=[
                {
                    "owner": "Ravi",
                    "action": "intro email",
                    "counterparty": "Mina",
                    "resolution": "sent",
                    "context": "Valid closure.",
                }
            ],
            decisions=[
                {
                    "owner": "Mina Lee",
                    "action": "move the launch review to Tuesday",
                    "counterparty": "Ravi",
                    "context": "Valid decision.",
                },
                {
                    "owner": "Team",
                    "action": "keep the review owner unchanged",
                    "counterparty": None,
                    "context": "Decision without a counterparty.",
                },
            ],
            relations=[
                {
                    "from": "Mina",
                    "to": "Ravi",
                    "kind": "works-with",
                    "note": "",
                    "quote": "Mina and Ravi will handle the deck.",
                },
                {
                    "from": "Mina",
                    "to": "Nobody Visible",
                    "kind": "knows",
                    "note": "Mina mentioned knowing someone outside the tracked entities.",
                    "quote": None,
                },
            ],
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert record["commitments"][0]["owner_entity_id"] == "mina_lee"
    assert record["commitments"][0]["counterparty_entity_id"] == "ravi_shah"
    assert record["commitments"][1]["owner_entity_id"] is None
    assert record["commitments"][1]["counterparty_entity_id"] is None
    assert record["closures"][0]["owner_entity_id"] == "ravi_shah"
    assert record["closures"][0]["counterparty_entity_id"] == "mina_lee"
    assert record["decisions"][0]["owner_entity_id"] == "mina_lee"
    assert record["decisions"][0]["counterparty"] == "Ravi"
    assert record["decisions"][0]["counterparty_entity_id"] == "ravi_shah"
    assert record["decisions"][1]["counterparty"] is None
    assert record["decisions"][1]["counterparty_entity_id"] is None
    assert record["relations"][0]["from_entity_id"] == "mina_lee"
    assert record["relations"][0]["to_entity_id"] == "ravi_shah"
    assert record["relations"][1]["to"] == "Nobody Visible"
    assert record["relations"][1]["to_entity_id"] is None
    assert record["relations"][1]["note"] == (
        "Mina mentioned knowing someone outside the tracked entities."
    )
    assert record["relations"][1]["quote"] is None
    assert record["commitments"][0]["owner"] == "Mina"
    assert record["closures"][0]["counterparty"] == "Mina"


def test_story_hook_ambiguous_entities_keep_names_and_null_ids(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record
    from solstone.think.entities import load_ambiguities

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    _write_detected_entities(
        tmp_path,
        "work",
        "20260418",
        [
            {"id": "sarah_connor", "type": "Person", "name": "Sarah Connor"},
            {"id": "sarah_lee", "type": "Person", "name": "Sarah Lee"},
        ],
    )
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            commitments=[
                {
                    "owner": "Sarah",
                    "action": "send the revised deck",
                    "counterparty": "Sarah",
                    "when": "Friday morning",
                    "context": "Ambiguous Sarah should not attach.",
                }
            ],
            closures=[],
            decisions=[],
            relations=[
                {
                    "from": "Sarah",
                    "to": "Sarah",
                    "kind": "works-with",
                    "note": "Ambiguous relationship.",
                }
            ],
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert record["commitments"][0]["owner"] == "Sarah"
    assert record["commitments"][0]["owner_entity_id"] is None
    assert record["commitments"][0]["counterparty_entity_id"] is None
    assert record["relations"][0]["from"] == "Sarah"
    assert record["relations"][0]["from_entity_id"] is None
    assert record["relations"][0]["to_entity_id"] is None
    rows = load_ambiguities()
    assert len(rows) == 1
    assert rows[0]["normalized_query"] == "sarah"


def test_story_hook_skips_invalid_relations(tmp_path, monkeypatch, caplog):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            relations=[
                {
                    "from": "Mina",
                    "to": "Ravi",
                    "kind": "works-with",
                    "note": "",
                    "quote": "They are paired on the deck.",
                },
                {
                    "from": "Mina",
                    "to": "Ravi",
                    "kind": "other",
                    "note": " ",
                    "quote": "They have a custom relationship.",
                },
                {
                    "from": "Mina",
                    "to": "Ravi",
                    "kind": "mentors",
                    "note": "Mina mentors Ravi.",
                    "quote": "Mina mentors Ravi.",
                },
            ]
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert [relation["kind"] for relation in record["relations"]] == ["works-with"]
    assert "other kind requires note" in caplog.text
    assert "invalid kind 'mentors'" in caplog.text


def test_story_hook_idempotent_rerun(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            relations=[
                {
                    "from": "Mina",
                    "to": "Ravi",
                    "kind": "works-with",
                    "note": "",
                    "quote": "Mina and Ravi owned the first pass.",
                }
            ]
        ),
        _context(tmp_path),
    )
    first = _load_record("work", "20260418")
    assert len(first["edits"]) == 1
    assert first["relations"][0]["kind"] == "works-with"

    post_process(
        _valid_result(
            body="Second pass with a clearer summary.",
            topics=["handoff"],
            commitments=[],
            closures=[],
            decisions=[
                {
                    "owner": "Lead",
                    "action": "ship the patch on Wednesday",
                    "counterparty": None,
                    "context": "The second pass reached a more specific plan.",
                }
            ],
            relations=[
                {
                    "from": "Lead",
                    "to": "Patch",
                    "kind": "created",
                    "note": "",
                    "quote": None,
                }
            ],
        ),
        _context(tmp_path),
    )

    second = _load_record("work", "20260418")
    assert second["story"] == {
        "talent": "conversation",
        "body": "Second pass with a clearer summary.",
        "topics": ["handoff"],
        "confidence": 0.82,
    }
    assert second["commitments"] == []
    assert second["closures"] == []
    assert second["decisions"] == [
        {
            "owner": "Lead",
            "action": "ship the patch on Wednesday",
            "counterparty": None,
            "context": "The second pass reached a more specific plan.",
            "owner_entity_id": None,
            "counterparty_entity_id": None,
        }
    ]
    assert second["relations"] == [
        {
            "from": "Lead",
            "to": "Patch",
            "kind": "created",
            "note": "",
            "quote": None,
            "from_entity_id": None,
            "to_entity_id": None,
        }
    ]
    assert len(second["edits"]) == 2
    assert second["edits"][-1]["fields"] == [
        "story",
        "commitments",
        "closures",
        "decisions",
        "relations",
    ]


def test_story_hook_normalizes_topics(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    post_process(
        _valid_result(
            topics=["Launch Plan", "follow-up", "LAUNCH plan", "   ", "follow-up"]
        ),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert record["story"]["topics"] == ["launch plan", "follow-up"]


def test_story_hook_clamps_confidence(tmp_path, monkeypatch, caplog):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record, load_activity_records

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    caplog.set_level(logging.WARNING)

    append_activity_record(
        "work",
        "20260418",
        _activity_record(record_id="meeting_090000_300"),
    )
    post_process(_valid_result(confidence=1.4), _context(tmp_path))
    first = _load_record("work", "20260418")
    assert first["story"]["confidence"] == 1.0
    assert "clamped" in caplog.text

    append_activity_record(
        "work",
        "20260418",
        _activity_record(record_id="meeting_100000_300"),
    )
    post_process(
        _valid_result(confidence=-0.2),
        _context(tmp_path, record_id="meeting_100000_300"),
    )
    records = load_activity_records("work", "20260418", include_hidden=True)
    by_id = {record["id"]: record for record in records}
    assert by_id["meeting_090000_300"]["story"]["confidence"] == 1.0
    assert by_id["meeting_100000_300"]["story"]["confidence"] == 0.0


def test_story_hook_rejects_nan_confidence(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    returned = post_process(
        _valid_result(confidence=float("nan")),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert returned == ""
    assert "story" not in record


def test_story_hook_rejects_non_numeric_confidence(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    returned = post_process(
        _valid_result(confidence="high"),
        _context(tmp_path),
    )

    record = _load_record("work", "20260418")
    assert returned == ""
    assert "story" not in record


def test_story_hook_missing_record_logs_and_returns(tmp_path, monkeypatch, caplog):
    from solstone.talent.story import post_process

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))

    returned = post_process(_valid_result(), _context(tmp_path))

    assert returned == ""
    assert "activity record not found" in caplog.text


def test_story_hook_no_json_file_written(tmp_path, monkeypatch):
    from solstone.talent.story import post_process
    from solstone.think.activities import append_activity_record

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    output_path = (
        tmp_path
        / "facets"
        / "work"
        / "activities"
        / "20260418"
        / "meeting_090000_300"
        / "story.json"
    )
    returned = post_process(_valid_result(), _context(tmp_path))

    assert returned == ""
    assert not output_path.exists()


def test_generate_seam_story_hook_writes_no_output_file(tmp_path, monkeypatch):
    from solstone.think.activities import append_activity_record
    from solstone.think.talents import _execute_generate

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    output_path = (
        tmp_path
        / "facets"
        / "work"
        / "activities"
        / "20260418"
        / "meeting_090000_300"
        / "conversation.json"
    )
    config = {
        **_context(tmp_path),
        "output": "json",
        "output_path": str(output_path),
        "hook": {"post": "story"},
        "prompt": "x",
    }
    monkeypatch.setattr(
        "solstone.think.models.generate_with_result",
        lambda **kwargs: {"text": _valid_result()},
    )

    # Exercises the talents.py write-seam gate removed from cortex in 56b5a121.
    asyncio.run(_execute_generate(config, lambda e: None))

    assert not output_path.exists()
    record = _load_record("work", "20260418")
    assert (
        record["story"]["body"] == "Aligned on launch work and assigned the follow-up."
    )
    assert record["edits"][-1]["actor"] == "story"


def test_generate_seam_participation_writes_json(tmp_path, monkeypatch):
    from solstone.think.activities import append_activity_record
    from solstone.think.talents import _execute_generate

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    append_activity_record("work", "20260418", _activity_record())

    output_path = (
        tmp_path
        / "facets"
        / "work"
        / "activities"
        / "20260418"
        / "meeting_090000_300"
        / "participation.json"
    )
    payload = {
        "participation": [{"name": "Mina", "role": "mentioned", "source": "text"}]
    }
    config = {
        **_context(tmp_path, name="participation"),
        "output": "json",
        "output_path": str(output_path),
        "hook": {"post": "participation"},
        "prompt": "x",
    }
    monkeypatch.setattr(
        "solstone.think.models.generate_with_result",
        lambda **kwargs: {"text": json.dumps(payload)},
    )

    # Exercises the talents.py write-seam gate removed from cortex in 56b5a121.
    asyncio.run(_execute_generate(config, lambda e: None))

    assert output_path.exists()
    assert output_path.stat().st_size > 0
    assert json.loads(output_path.read_text()) == payload
