namespace Inspectron.Epson.TemplateEngine.Tests;

public class TemplateRendererTests
{
    private readonly ReceiptTemplateEngine _engine = new();

    [Fact]
    public void Render_EmptyReceipt()
    {
        var commands = _engine.Render("<receipt></receipt>", "{}", 42, 22);
        Assert.Empty(commands);
    }

    [Fact]
    public void Render_SimpleLine()
    {
        var commands = _engine.Render(
            "<receipt><line>Hello World</line></receipt>",
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.Equal("Hello World", commands[0].Text);
    }

    [Fact]
    public void Render_LineWithDataBinding()
    {
        var commands = _engine.Render(
            "<receipt><line>Hello {{Name}}</line></receipt>",
            """{"Name":"John"}""", 42, 22);

        Assert.Single(commands);
        Assert.Equal("Hello John", commands[0].Text);
    }

    [Fact]
    public void Render_LineWithFormatting()
    {
        var commands = _engine.Render(
            """<receipt><line bold="true" big="true" tall="true" red="true">Text</line></receipt>""",
            "{}", 42, 22);

        var cmd = commands[0];
        Assert.True(cmd.IsBold);
        Assert.True(cmd.IsBig);
        Assert.True(cmd.IsTall);
        Assert.True(cmd.IsRed);
    }

    [Fact]
    public void Render_LineWithCenterAlignment()
    {
        var commands = _engine.Render(
            """<receipt><line align="center">Test</line></receipt>""",
            "{}", 42, 22);

        Assert.Equal("                   Test", commands[0].Text);
    }

    [Fact]
    public void Render_LineWithCenterAlignment_BigFont()
    {
        var commands = _engine.Render(
            """<receipt><line align="center" big="true">Test</line></receipt>""",
            "{}", 42, 22);

        Assert.Equal("         Test", commands[0].Text);
    }

    [Fact]
    public void Render_EmptyLine()
    {
        var commands = _engine.Render(
            "<receipt><line /></receipt>",
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.Equal("", commands[0].Text);
    }

    [Fact]
    public void Render_Separator()
    {
        var commands = _engine.Render(
            "<receipt><separator /></receipt>",
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.Equal(new string('-', 42), commands[0].Text);
    }

    [Fact]
    public void Render_Cut()
    {
        var commands = _engine.Render(
            "<receipt><cut /></receipt>",
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.True(commands[0].IsCut);
    }

    [Fact]
    public void Render_Feed()
    {
        var commands = _engine.Render(
            """<receipt><feed lines="3" /></receipt>""",
            "{}", 42, 22);

        Assert.Equal(3, commands.Count);
        Assert.All(commands, c => Assert.Equal("", c.Text));
    }

    [Fact]
    public void Render_Columns()
    {
        var commands = _engine.Render(
            """<receipt><columns left="Name" right="Price" /></receipt>""",
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.Equal(42, commands[0].Text.Length);
        Assert.StartsWith("Name", commands[0].Text);
        Assert.EndsWith("Price", commands[0].Text);
    }

    [Fact]
    public void Render_ColumnsWithDataBinding()
    {
        var commands = _engine.Render(
            """<receipt><columns left="{{Label}}" right="{{Value}}" /></receipt>""",
            """{"Label":"Total","Value":"100.00"}""", 42, 22);

        Assert.Single(commands);
        Assert.Contains("Total", commands[0].Text);
        Assert.Contains("100.00", commands[0].Text);
    }

    [Fact]
    public void Render_Foreach()
    {
        var commands = _engine.Render("""
            <receipt>
              <foreach items="Items" var="item">
                <line>{{item.Name}}</line>
              </foreach>
            </receipt>
            """,
            """{"Items":[{"Name":"Pizza"},{"Name":"Pasta"},{"Name":"Salad"}]}""",
            42, 22);

        Assert.Equal(3, commands.Count);
        Assert.Equal("Pizza", commands[0].Text);
        Assert.Equal("Pasta", commands[1].Text);
        Assert.Equal("Salad", commands[2].Text);
    }

    [Fact]
    public void Render_Foreach_EmptyArray()
    {
        var commands = _engine.Render("""
            <receipt>
              <foreach items="Items" var="item">
                <line>{{item.Name}}</line>
              </foreach>
            </receipt>
            """,
            """{"Items":[]}""", 42, 22);

        Assert.Empty(commands);
    }

    [Fact]
    public void Render_Foreach_WithLoopVariables()
    {
        var commands = _engine.Render("""
            <receipt>
              <foreach items="Items" var="item">
                <line>{{$index}}: {{item.Name}}</line>
              </foreach>
            </receipt>
            """,
            """{"Items":[{"Name":"A"},{"Name":"B"},{"Name":"C"}]}""",
            42, 22);

        Assert.Equal(3, commands.Count);
        Assert.Equal("0: A", commands[0].Text);
        Assert.Equal("1: B", commands[1].Text);
        Assert.Equal("2: C", commands[2].Text);
    }

    [Fact]
    public void Render_NestedForeach()
    {
        var commands = _engine.Render("""
            <receipt>
              <foreach items="Groups" var="group">
                <line>{{group.Name}}</line>
                <foreach items="group.Items" var="item">
                  <line>  {{item.Name}}</line>
                </foreach>
              </foreach>
            </receipt>
            """,
            """{"Groups":[{"Name":"G1","Items":[{"Name":"A"},{"Name":"B"}]},{"Name":"G2","Items":[{"Name":"C"}]}]}""",
            42, 22);

        Assert.Equal(5, commands.Count);
        Assert.Equal("G1", commands[0].Text);
        Assert.Equal("  A", commands[1].Text);
        Assert.Equal("  B", commands[2].Text);
        Assert.Equal("G2", commands[3].Text);
        Assert.Equal("  C", commands[4].Text);
    }

    [Fact]
    public void Render_If_True()
    {
        var commands = _engine.Render("""
            <receipt>
              <if test="Name">
                <line>Has name: {{Name}}</line>
              </if>
            </receipt>
            """,
            """{"Name":"John"}""", 42, 22);

        Assert.Single(commands);
        Assert.Equal("Has name: John", commands[0].Text);
    }

    [Fact]
    public void Render_If_False()
    {
        var commands = _engine.Render("""
            <receipt>
              <if test="Name">
                <line>Has name</line>
              </if>
            </receipt>
            """,
            """{}""", 42, 22);

        Assert.Empty(commands);
    }

    [Fact]
    public void Render_IfElse_True()
    {
        var commands = _engine.Render("""
            <receipt>
              <if test="Name">
                <line>Yes</line>
              </if>
              <else>
                <line>No</line>
              </else>
            </receipt>
            """,
            """{"Name":"John"}""", 42, 22);

        Assert.Single(commands);
        Assert.Equal("Yes", commands[0].Text);
    }

    [Fact]
    public void Render_IfElse_False()
    {
        var commands = _engine.Render("""
            <receipt>
              <if test="Name">
                <line>Yes</line>
              </if>
              <else>
                <line>No</line>
              </else>
            </receipt>
            """,
            """{}""", 42, 22);

        Assert.Single(commands);
        Assert.Equal("No", commands[0].Text);
    }

    [Fact]
    public void Render_If_NegatedCondition()
    {
        var commands = _engine.Render("""
            <receipt>
              <if test="!$last">
                <cut />
              </if>
            </receipt>
            """,
            """{}""", 42, 22);

        // $last is not set so not a boolean false, but the variable doesn't exist
        // When there's no foreach context, $last is null, so !null = true
        Assert.Single(commands);
        Assert.True(commands[0].IsCut);
    }

    [Fact]
    public void Render_Foreach_WithIfNotLast()
    {
        var commands = _engine.Render("""
            <receipt>
              <foreach items="Items" var="item">
                <line>{{item.Name}}</line>
                <if test="!$last">
                  <separator />
                </if>
              </foreach>
            </receipt>
            """,
            """{"Items":[{"Name":"A"},{"Name":"B"},{"Name":"C"}]}""",
            42, 22);

        // A, sep, B, sep, C = 5
        Assert.Equal(5, commands.Count);
        Assert.Equal("A", commands[0].Text);
        Assert.Equal(new string('-', 42), commands[1].Text);
        Assert.Equal("B", commands[2].Text);
        Assert.Equal(new string('-', 42), commands[3].Text);
        Assert.Equal("C", commands[4].Text);
    }

    [Fact]
    public void Render_LineWithWrap()
    {
        var commands = _engine.Render(
            """<receipt><line wrap="true">The quick brown fox jumps over the lazy dog and more text here</line></receipt>""",
            "{}", 30, 15);

        Assert.True(commands.Count > 1);
    }

    [Fact]
    public void Render_LineSpacing()
    {
        var commands = _engine.Render(
            """<receipt><line lineSpacing="50">Text</line></receipt>""",
            "{}", 42, 22);

        Assert.Equal(50, commands[0].SetLineSpacing);
    }

    [Fact]
    public void Render_FormatString()
    {
        var commands = _engine.Render(
            "<receipt><line>{{Price:F2}}</line></receipt>",
            """{"Price":9.5}""", 42, 22);

        Assert.Equal("9.50", commands[0].Text);
    }

    [Fact]
    public void Render_DateTimeFormat()
    {
        var commands = _engine.Render(
            "<receipt><line>{{Date:dd.MM.yyyy}}</line></receipt>",
            """{"Date":"2024-03-15T14:30:00Z"}""", 42, 22);

        Assert.Equal("15.03.2024", commands[0].Text);
    }

    [Fact]
    public void Render_Row()
    {
        var commands = _engine.Render("""
            <receipt>
              <row>
                <col width="14">Left</col>
                <col width="14" align="center">Center</col>
                <col width="14" align="right">Right</col>
              </row>
            </receipt>
            """,
            "{}", 42, 22);

        Assert.Single(commands);
        Assert.Equal(42, commands[0].Text.Length);
    }

    [Fact]
    public void Render_InvalidXml_ThrowsParsingException()
    {
        Assert.Throws<TemplateParsingException>(() =>
            _engine.Render("not xml", "{}", 42, 22));
    }

    [Fact]
    public void Render_InvalidJson_ThrowsRenderingException()
    {
        Assert.Throws<TemplateRenderingException>(() =>
            _engine.Render("<receipt></receipt>", "not json", 42, 22));
    }

    [Fact]
    public void Render_ComplexTemplate()
    {
        var template = """
            <receipt>
              <line align="center" big="true" red="true">Restaurant</line>
              <separator />
              <line />
              <line align="center">{{DateTime:dd-MMM-yy HH:mm}} Nr.:{{Number}}</line>
              <line align="center" big="true" bold="true">Tisch: {{Table}}</line>
              <separator />
              <foreach items="Items" var="item">
                <line bold="true">{{item.Qty}}x {{item.Name}}</line>
              </foreach>
              <separator />
              <columns left="Total:" right="{{Total:F2}} CHF" bold="true" />
              <cut />
            </receipt>
            """;

        var json = """
            {
              "DateTime": "2024-03-15T14:30:00Z",
              "Number": "42",
              "Table": "5",
              "Items": [
                {"Qty": 2, "Name": "Margherita"},
                {"Qty": 1, "Name": "Tiramisu"}
              ],
              "Total": 45.50
            }
            """;

        var commands = _engine.Render(template, json, 42, 22);

        Assert.True(commands.Count >= 10);

        // Header
        Assert.True(commands[0].IsRed);
        Assert.True(commands[0].IsBig);
        Assert.Contains("Restaurant", commands[0].Text);

        // Last command is cut
        Assert.True(commands[^1].IsCut);
    }
}
