# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
# See LICENSE for details.
"""Tests for TemplateManager."""

import pytest

from vcti.template import (
    TemplateManager,
    TemplateNotFoundError,
    TemplateRegistry,
    TemplateRenderError,
    TemplateSyntaxError,
)


@pytest.fixture
def registry():
    reg = TemplateRegistry()
    reg.register("greeting", "Hello, {{ name }}!")
    reg.register("list", "{% for item in items %}{{ item }} {% endfor %}")
    return reg


@pytest.fixture
def manager(registry):
    return TemplateManager(registry=registry)


@pytest.fixture
def fs_templates(tmp_path):
    """Create template files on disk."""
    tmpl = tmp_path / "report.html"
    tmpl.write_text("<h1>{{ title }}</h1><p>{{ body }}</p>")
    partial = tmp_path / "row.html"
    partial.write_text("<tr><td>{{ value }}</td></tr>")
    return tmp_path


# ------------------------------------------------------------------
# Basic rendering
# ------------------------------------------------------------------


class TestRender:
    def test_render_from_registry(self, manager):
        result = manager.render("greeting", name="World")
        assert result == "Hello, World!"

    def test_render_missing_raises(self, manager):
        with pytest.raises(TemplateNotFoundError, match="not found"):
            manager.render("nonexistent")

    def test_render_missing_is_lookup_error(self, manager):
        """TemplateNotFoundError inherits LookupError."""
        with pytest.raises(LookupError):
            manager.render("nonexistent")

    def test_render_with_list(self, manager):
        result = manager.render("list", items=["a", "b", "c"])
        assert result == "a b c "


class TestRenderString:
    def test_render_string(self, manager):
        result = manager.render_string("{{ x }} + {{ y }}", x=1, y=2)
        assert result == "1 + 2"

    def test_render_string_no_registry_needed(self):
        tm = TemplateManager()
        result = tm.render_string("Hello, {{ name }}!", name="World")
        assert result == "Hello, World!"


# ------------------------------------------------------------------
# Filesystem
# ------------------------------------------------------------------


class TestRenderFromFilesystem:
    def test_render_file_template(self, registry, fs_templates):
        tm = TemplateManager(registry=registry, template_dirs=[fs_templates])
        result = tm.render("report.html", title="Q1", body="Revenue up")
        assert "<h1>Q1</h1>" in result
        assert "<p>Revenue up</p>" in result

    def test_registry_takes_precedence(self, fs_templates):
        reg = TemplateRegistry()
        reg.register("report.html", "REGISTRY: {{ title }}")
        tm = TemplateManager(registry=reg, template_dirs=[fs_templates])
        result = tm.render("report.html", title="Q1")
        assert result == "REGISTRY: Q1"

    def test_filesystem_only(self, fs_templates):
        tm = TemplateManager(template_dirs=[fs_templates])
        result = tm.render("report.html", title="Q1", body="test")
        assert "<h1>Q1</h1>" in result

    def test_filesystem_missing_falls_through(self, fs_templates):
        tm = TemplateManager(template_dirs=[fs_templates])
        with pytest.raises(TemplateNotFoundError, match="not found"):
            tm.render("no_such_file.html")


# ------------------------------------------------------------------
# Streaming
# ------------------------------------------------------------------


class TestRenderStreaming:
    def test_streaming_chunks(self, manager):
        chunks = [{"items": ["a", "b"]}, {"items": ["c", "d"]}]
        fragments = list(manager.render_streaming("list", chunks))
        assert len(fragments) == 2
        assert fragments[0] == "a b "
        assert fragments[1] == "c d "

    def test_streaming_with_header_footer(self):
        reg = TemplateRegistry()
        reg.register("header", "<table>{{ title }}")
        reg.register("row", "<tr>{{ value }}</tr>")
        reg.register("footer", "</table>")

        tm = TemplateManager(registry=reg)
        fragments = list(
            tm.render_streaming(
                "row",
                chunks=[{"value": "A"}, {"value": "B"}],
                header_template="header",
                header_context={"title": "Data"},
                footer_template="footer",
                footer_context={},
            )
        )
        assert fragments == ["<table>Data", "<tr>A</tr>", "<tr>B</tr>", "</table>"]

    def test_streaming_empty_chunks(self, manager):
        fragments = list(manager.render_streaming("greeting", []))
        assert fragments == []

    def test_streaming_header_only(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ x }}")
        tm = TemplateManager(registry=reg)
        fragments = list(
            tm.render_streaming(
                "t",
                [],
                header_context={"x": "start"},
            )
        )
        assert fragments == ["start"]

    def test_streaming_footer_only(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ x }}")
        tm = TemplateManager(registry=reg)
        fragments = list(
            tm.render_streaming(
                "t",
                [],
                footer_context={"x": "end"},
            )
        )
        assert fragments == ["end"]


# ------------------------------------------------------------------
# Strict mode
# ------------------------------------------------------------------


class TestStrictMode:
    def test_strict_undefined_raises(self):
        tm = TemplateManager(strict=True)
        with pytest.raises(TemplateRenderError, match="'name' is undefined"):
            tm.render_string("Hello, {{ name }}!")

    def test_strict_false_renders_empty(self):
        tm = TemplateManager(strict=False)
        result = tm.render_string("Hello, {{ name }}!")
        assert result == "Hello, !"

    def test_strict_property(self):
        assert TemplateManager(strict=True).strict is True
        assert TemplateManager().strict is False

    def test_strict_render_from_registry(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ missing }}")
        tm = TemplateManager(registry=reg, strict=True)
        with pytest.raises(TemplateRenderError):
            tm.render("t")

    def test_strict_streaming_raises(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ x }}")
        tm = TemplateManager(registry=reg, strict=True)
        gen = tm.render_streaming("t", [{"x": "ok"}, {}])
        assert next(gen) == "ok"
        with pytest.raises(TemplateRenderError):
            next(gen)


# ------------------------------------------------------------------
# Custom filters and globals
# ------------------------------------------------------------------


class TestFiltersAndGlobals:
    def test_custom_filter_in_render(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ name|shout }}")
        tm = TemplateManager(
            registry=reg,
            filters={"shout": lambda s: s.upper() + "!!!"},
        )
        assert tm.render("t", name="hello") == "HELLO!!!"

    def test_custom_filter_in_render_string(self):
        tm = TemplateManager(filters={"double": lambda x: x * 2})
        assert tm.render_string("{{ val|double }}", val=3) == "6"

    def test_custom_filter_in_filesystem(self, fs_templates):
        (fs_templates / "f.html").write_text("{{ v|upper }}")
        tm = TemplateManager(
            template_dirs=[fs_templates],
            filters={"upper": str.upper},
        )
        assert tm.render("f.html", v="test") == "TEST"

    def test_custom_globals(self):
        tm = TemplateManager(globals={"app": "vcti"})
        assert tm.render_string("Built by {{ app }}") == "Built by vcti"

    def test_globals_available_in_registry(self):
        reg = TemplateRegistry()
        reg.register("t", "v{{ version }}")
        tm = TemplateManager(registry=reg, globals={"version": "2.0"})
        assert tm.render("t") == "v2.0"


# ------------------------------------------------------------------
# Autoescape
# ------------------------------------------------------------------


class TestAutoescape:
    def test_html_autoescape_render_string(self):
        tm = TemplateManager()
        result = tm.render_string("{{ tag }}", tag="<b>bold</b>")
        # Default autoescape includes html — tags should be escaped.
        assert "&lt;b&gt;" in result

    def test_html_autoescape_registry(self):
        reg = TemplateRegistry()
        reg.register("t", "{{ tag }}")
        tm = TemplateManager(registry=reg)
        result = tm.render("t", tag="<script>alert(1)</script>")
        assert "<script>" not in result
        assert "&lt;script&gt;" in result

    def test_custom_autoescape_extensions(self, fs_templates):
        (fs_templates / "note.txt").write_text("{{ tag }}")
        tm = TemplateManager(
            template_dirs=[fs_templates],
            autoescape_extensions=["txt"],
        )
        result = tm.render("note.txt", tag="<b>bold</b>")
        assert "&lt;b&gt;" in result


# ------------------------------------------------------------------
# Syntax errors
# ------------------------------------------------------------------


class TestSyntaxErrors:
    def test_render_string_syntax_error(self):
        tm = TemplateManager()
        with pytest.raises(TemplateSyntaxError):
            tm.render_string("{% if %}")

    def test_registry_syntax_error(self):
        reg = TemplateRegistry()
        reg.register("bad", "{% for x %}")
        tm = TemplateManager(registry=reg)
        with pytest.raises(TemplateSyntaxError, match="Syntax error"):
            tm.render("bad")


# ------------------------------------------------------------------
# Jinja2 template features
# ------------------------------------------------------------------


class TestJinjaFeatures:
    def test_conditional(self):
        tm = TemplateManager()
        tmpl = "{% if show %}visible{% else %}hidden{% endif %}"
        assert tm.render_string(tmpl, show=True) == "visible"
        assert tm.render_string(tmpl, show=False) == "hidden"

    def test_nested_loop(self):
        tm = TemplateManager()
        tmpl = "{% for row in rows %}{% for c in row %}{{ c }}{% endfor %};{% endfor %}"
        result = tm.render_string(tmpl, rows=[["a", "b"], ["c"]])
        assert result == "ab;c;"

    def test_builtin_filter(self):
        tm = TemplateManager()
        assert tm.render_string("{{ name|upper }}", name="world") == "WORLD"

    def test_default_filter(self):
        tm = TemplateManager()
        result = tm.render_string("{{ x|default('N/A') }}")
        assert result == "N/A"

    @pytest.mark.parametrize(
        ("template", "ctx", "expected"),
        [
            ("{{ x + y }}", {"x": 1, "y": 2}, "3"),
            ("{{ name|length }}", {"name": "abc"}, "3"),
            ("{{ items|join(', ') }}", {"items": ["a", "b"]}, "a, b"),
            ("{{ x if x else 'none' }}", {"x": ""}, "none"),
            ("{{ '%d' % n }}", {"n": 42}, "42"),
        ],
    )
    def test_expressions(self, template, ctx, expected):
        tm = TemplateManager()
        assert tm.render_string(template, **ctx) == expected


# ------------------------------------------------------------------
# Empty / edge-case managers
# ------------------------------------------------------------------


class TestManagerNoSources:
    def test_empty_manager_render_raises(self):
        tm = TemplateManager()
        with pytest.raises(TemplateNotFoundError, match="not found"):
            tm.render("anything")

    def test_empty_manager_render_string_works(self):
        tm = TemplateManager()
        assert tm.render_string("{{ x }}", x=42) == "42"


# ------------------------------------------------------------------
# repr
# ------------------------------------------------------------------


class TestManagerRepr:
    def test_repr_with_registry(self, manager):
        r = repr(manager)
        assert "TemplateManager" in r
        assert "registry" in r

    def test_repr_with_filesystem(self, fs_templates):
        tm = TemplateManager(template_dirs=[fs_templates])
        assert "filesystem" in repr(tm)

    def test_repr_empty(self):
        tm = TemplateManager()
        assert "empty" in repr(tm)

    def test_repr_both(self, registry, fs_templates):
        tm = TemplateManager(registry=registry, template_dirs=[fs_templates])
        r = repr(tm)
        assert "registry" in r
        assert "filesystem" in r

    def test_repr_strict(self):
        tm = TemplateManager(strict=True)
        assert "strict" in repr(tm)
