from datetime import date

import pytest

from . import (
    Alias,
    Canvas,
    CanvasName,
    CanvasNodeType,
    CanvasService,
    ContentDiscoveryService,
    DefaultVaultPolicy,
    InvalidCanvasError,
    InvalidLinkError,
    InvalidNoteError,
    InvalidReferenceError,
    InvalidSearchError,
    InvalidTagError,
    InvalidTaskError,
    InvalidTemplateError,
    InvalidVaultError,
    Link,
    Note,
    NotePath,
    NoteState,
    NoteTitle,
    Task,
    TaskService,
    TaskState,
    TaskText,
    TagName,
    Template,
    TemplateName,
    TemplateService,
    Vault,
    VaultName,
    VaultService,
)


def test_obsidian_value_objects_validate_and_normalize() -> None:
    assert VaultName("  Main Vault ").value == "Main Vault"
    assert NoteTitle("  Weekly Review ").value == "Weekly Review"
    assert NotePath("  Projects\\Q1\\Roadmap.md ").value == "Projects/Q1/Roadmap.md"
    assert TagName("  #Work ").value == "work"
    assert Alias("  Board ").value == "Board"

    with pytest.raises(InvalidVaultError):
        VaultName("")

    with pytest.raises(InvalidNoteError):
        NoteTitle("")

    with pytest.raises(InvalidReferenceError):
        NotePath("../escape.md")

    with pytest.raises(InvalidTagError):
        TagName(" ")


def test_note_and_vault_manage_lifecycle_and_uniqueness() -> None:
    vault = Vault(name=VaultName("Main"))
    note = Note(
        title=NoteTitle("Project"),
        path=NotePath("Projects/Project.md"),
        content="",
    )

    vault.add_note(note)
    assert note.vault_name == VaultName("Main")

    note.add_tag(TagName("#work"))
    note.add_alias(Alias("Project Board"))
    note.add_link(Link(target=NotePath("Refs/Plan.md"), alias=Alias("plan")))
    assert vault.find_by_tag(TagName("work")) == [note]

    same_path = vault.move_note(NotePath("Projects/Project.md"), NotePath("Projects/Project.md"))
    assert same_path is note

    vault.archive_note(NotePath("Projects/Project.md"))
    assert note.state == NoteState.ARCHIVED
    vault.trash_note(NotePath("Projects/Project.md"))
    assert note.state == NoteState.TRASHED
    vault.restore_note(NotePath("Projects/Project.md"))
    assert note.state == NoteState.ACTIVE

    with pytest.raises(InvalidVaultError):
        vault.add_note(Note(title=NoteTitle("Duplicate"), path=NotePath("Projects/Project.md")))


def test_content_discovery_extracts_links_and_tags() -> None:
    service = ContentDiscoveryService()
    result = service.discover(
        "Link to [[Specs/Board#Overview|board]] and ![[Images/diagram.png]] with #Work and #work."
    )

    assert result.links[0].target == NotePath("Specs/Board")
    assert result.links[0].alias == Alias("board")
    assert result.links[0].fragment == "Overview"
    assert result.links[1].is_embed is True
    assert [tag.value for tag in result.tags] == ["work"]


def test_vault_service_search_backlinks_and_daily_note() -> None:
    vault = Vault(name=VaultName("Main"))
    roadmap = Note(
        title=NoteTitle("Roadmap"),
        path=NotePath("Projects/Roadmap.md"),
        content="See [[Projects/Kanban.md]] and #work",
    )
    kanban = Note(
        title=NoteTitle("Kanban"),
        path=NotePath("Projects/Kanban.md"),
        content="Board details",
    )

    service = VaultService()
    service.add_note(vault, roadmap)
    service.add_note(vault, kanban)
    service.sync_note_metadata(roadmap)

    assert service.search(vault, "road").count(roadmap) == 1
    assert service.backlinks(vault, NotePath("Projects/Kanban.md")) == [roadmap]

    daily = service.create_daily_note(vault, on_date=date(2026, 4, 4))
    assert daily.path == NotePath("Daily/2026-04-04.md")
    assert TagName("daily") in daily.tags


def test_template_rendering_and_note_creation() -> None:
    template = Template(
        name=TemplateName("Daily"),
        body="# {{title}}\n{{body}}\n- [ ] Follow up",
        frontmatter={"type": "daily", "status": "open"},
        description="Daily note template",
    )
    service = TemplateService()

    rendered = service.render(template, {"title": "2026-04-04", "body": "Plan the day"})
    note = service.create_note_from_template(
        template,
        title=NoteTitle("2026-04-04"),
        path=NotePath("Daily/2026-04-04.md"),
        context={"title": "2026-04-04", "body": "Plan the day"},
        frontmatter={"owner": "Soi Gia"},
    )

    assert rendered == "# 2026-04-04\nPlan the day\n- [ ] Follow up"
    assert note.content.startswith("---\ntype: daily\nstatus: open\nowner: Soi Gia\n---\n")
    assert note.content.endswith(rendered)
    assert note.title == NoteTitle("2026-04-04")
    assert note.frontmatter["type"] == "daily"
    assert note.frontmatter["status"] == "open"
    assert note.frontmatter["owner"] == "Soi Gia"
    assert "{{title}}" not in note.content
    assert note.frontmatter["task_count"] == 1
    assert note.frontmatter["open_task_count"] == 1
    assert note.frontmatter["done_task_count"] == 0
    assert note.frontmatter["tasks"][0]["text"] == "Follow up"


def test_task_service_extracts_and_updates_tasks() -> None:
    note = Note(
        title=NoteTitle("Checklist"),
        path=NotePath("Projects/Checklist.md"),
        content="- [ ] Buy milk\n- [x] Pay bill",
        tags=[TagName("work")],
    )
    service = TaskService()

    tasks = service.extract_from_note(note)

    assert [task.state for task in tasks] == [TaskState.OPEN, TaskState.DONE]
    assert tasks[0].note_path == note.path
    assert tasks[1].completed_at is not None

    service.complete(tasks[0], completed_at=date(2026, 4, 4))
    assert tasks[0].state == TaskState.DONE
    assert tasks[0].completed_at == date(2026, 4, 4)

    service.reopen(tasks[0])
    assert tasks[0].state == TaskState.OPEN
    assert tasks[0].completed_at is None


def test_task_service_syncs_tasks_into_note_frontmatter() -> None:
    note = Note(
        title=NoteTitle("Checklist"),
        path=NotePath("Projects/Checklist.md"),
        content="- [ ] Buy milk\n- [x] Pay bill",
        tags=[TagName("work")],
    )
    service = TaskService()

    tasks = service.sync_note_tasks(note)

    assert len(tasks) == 2
    assert note.frontmatter["task_count"] == 2
    assert note.frontmatter["open_task_count"] == 1
    assert note.frontmatter["done_task_count"] == 1
    assert note.frontmatter["tasks"][0]["text"] == "Buy milk"
    assert note.frontmatter["tasks"][0]["state"] == TaskState.OPEN.value
    assert note.frontmatter["tasks"][1]["state"] == TaskState.DONE.value


def test_canvas_service_links_nodes_and_notes() -> None:
    canvas = Canvas(name=CanvasName("Knowledge Map"))
    note = Note(title=NoteTitle("Home"), path=NotePath("Home.md"))
    service = CanvasService()

    note_node = service.add_note_node(canvas, node_id="note-1", note=note, x=100, y=100)
    text_node = service.add_text_node(canvas, node_id="text-1", text="Start here", x=280, y=100)
    edge = service.link(canvas, from_id="text-1", to_id="note-1", label="refs")
    group_node = service.group(canvas, group_id="group-1", node_ids=["note-1", "text-1"])

    assert note_node.kind == CanvasNodeType.NOTE
    assert text_node.kind == CanvasNodeType.TEXT
    assert edge.label == "refs"
    assert canvas.notes() == [note.path]
    assert group_node.kind == CanvasNodeType.GROUP
    assert group_node.member_ids == ["note-1", "text-1"]
    assert service.get_node(canvas, "note-1").id == "note-1"
    assert service.get_edge(canvas, "text-1", "note-1").label == "refs"

    service.layout_horizontal(canvas, start_x=50, start_y=75, gap=200)
    assert [node.x for node in canvas.nodes[:2]] == [50, 250]
    assert [node.y for node in canvas.nodes[:2]] == [75, 75]
    service.layout_grid(canvas, columns=2, start_x=10, start_y=20, gap_x=100, gap_y=50)
    assert [node.x for node in canvas.nodes] == [10, 110, 10]
    assert [node.y for node in canvas.nodes] == [20, 20, 70]


def test_canvas_remove_operations_cascade_edges() -> None:
    canvas = Canvas(name=CanvasName("Removal Map"))
    service = CanvasService()
    service.add_text_node(canvas, node_id="n1", text="One", x=0, y=0)
    service.add_text_node(canvas, node_id="n2", text="Two", x=10, y=10)
    service.link(canvas, from_id="n1", to_id="n2", label="refs")

    removed_edge = service.remove_edge(canvas, "n1", "n2")
    assert removed_edge.label == "refs"
    assert canvas.edges == []

    service.link(canvas, from_id="n1", to_id="n2", label="refs")
    removed_node = service.remove_node(canvas, "n1")
    assert removed_node.id == "n1"
    assert canvas.edges == []

    with pytest.raises(InvalidCanvasError):
        service.remove_node(canvas, "missing")

    with pytest.raises(InvalidCanvasError):
        service.remove_edge(canvas, "n1", "n2")


def test_canvas_boundaries_are_enforced() -> None:
    canvas = Canvas(name=CanvasName("Boundary Map"))
    service = CanvasService()
    service.add_text_node(canvas, node_id="n1", text="One", x=0, y=0)
    service.add_text_node(canvas, node_id="n2", text="Two", x=10, y=10)

    with pytest.raises(InvalidCanvasError):
        service.group(canvas, group_id="group-dup", node_ids=["n1", "n1"])

    with pytest.raises(InvalidCanvasError):
        service.get_node(canvas, "missing")

    with pytest.raises(InvalidCanvasError):
        service.get_edge(canvas, "n1", "missing")

    with pytest.raises(InvalidCanvasError):
        service.layout_horizontal(canvas, gap=0)

    with pytest.raises(InvalidCanvasError):
        service.layout_grid(canvas, columns=0)


def test_invalid_obsidian_inputs_are_rejected() -> None:
    with pytest.raises(InvalidNoteError):
        Note(title="bad", path=NotePath("note.md"))  # type: ignore[arg-type]

    with pytest.raises(InvalidLinkError):
        Link(target="bad")  # type: ignore[arg-type]

    with pytest.raises(InvalidSearchError):
        VaultService().search(Vault(name=VaultName("Main")), " ")

    with pytest.raises(TypeError):
        DefaultVaultPolicy().can_add_note("bad", None)  # type: ignore[arg-type]

    with pytest.raises(InvalidTemplateError):
        Template(name="bad", body="ok")  # type: ignore[arg-type]

    with pytest.raises(InvalidTaskError):
        Task(text="bad")  # type: ignore[arg-type]

    with pytest.raises(InvalidCanvasError):
        Canvas(name="bad")  # type: ignore[arg-type]
