require File.expand_path('../helper', __FILE__)

describe "Parsers" do
  let(:template_root) { File.expand_path("../fixtures", __FILE__)                     }
  def read_template(template)
    File.read(File.join(template_root, template))
  end

  describe "First pass parser" do

    subject { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }

    it "will parse & execute a simple template with expressions" do
      context = ContextHash(right: "right", code: "<tag/>")
      result = subject.render("expressions1", context)
      result.must_equal "This is right &lt;tag/&gt;\n"
    end

    it "will parse & execute a simple template with statements" do
      context = ContextHash(right: "right")
      result = subject.render("statements1", context)
      result.must_equal "\nThis is right 0\n\nThis is right 1\n\nThis is right 2\n\n"
    end

    it "will parse & execute a simple template with comments" do
      context = ContextHash(right: "right")
      result = subject.render("comments1", context)
      result.must_equal "\n"
    end

    it "will remove whitespace after tags with a closing '-'" do
      context = ContextHash(right: "right")
      result = subject.render("whitespace1", context)
      expected = ["aa", "here 0", "here 1", "here 2\n", "ac\n", "ad\n", "ae\n", "af\n", "ag\n"].join("\n")
      result.must_equal expected
    end

    it "will render an object that responds to #read" do
      template = StringIO.new("This is ${ right }")
      context = ContextHash(right: "right")
      result = subject.render(template, context)
      result.must_equal "This is right"
    end


    it "can convert a template to another syntax" do
      result = subject.convert('statements1', Cutaneous::SecondPassSyntax)
      result.must_equal read_template('statements2.html.cut')
    end

    it "can convert a template string to another syntax" do
      result = subject.convert_string(read_template('statements1.html.cut'), Cutaneous::SecondPassSyntax)
      result.must_equal read_template('statements2.html.cut')
    end

    it "can convert a template proc to another syntax" do
      result = subject.convert(proc { read_template('statements1.html.cut') }, Cutaneous::SecondPassSyntax)
      result.must_equal read_template('statements2.html.cut')
    end
  end

  describe "Second pass parser" do
    let(:template_root) { File.expand_path("../fixtures", __FILE__)                     }
    subject { Cutaneous::Engine.new(template_root, Cutaneous::SecondPassSyntax, "html") }

    it "will parse & execute a simple template with expressions" do
      context = ContextHash(right: "right", code: "<tag/>")
      result = subject.render("expressions2", context)
      result.must_equal "This is right &lt;tag/&gt; <tag/>\n"
    end

    it "will parse & execute a simple template with statements" do
      context = ContextHash(right: "right")
      result = subject.render("statements2", context)
      result.must_equal "\nThis is right 0\n\nThis is right 1\n\nThis is right 2\n\n"
    end

    it "will parse & execute a simple template with comments" do
      context = ContextHash(right: "right")
      result = subject.render("comments2", context)
      result.must_equal "\n"
    end

    it "will remove whitespace after tags with a closing '-'" do
      context = ContextHash(right: "right")
      result = subject.render("whitespace2", context)
      expected = ["here 0", "here 1", "here 2\n"].join("\n")
      result.must_equal expected
    end

    it "can convert a template to another syntax" do
      result = subject.convert('statements2', Cutaneous::FirstPassSyntax)
      result.must_equal read_template('statements1.html.cut')
    end

    it "can convert a template string to another syntax" do
      result = subject.convert_string(read_template('statements2.html.cut'), Cutaneous::FirstPassSyntax)
      result.must_equal read_template('statements1.html.cut')
    end

    it "can convert a template proc to another syntax" do
      result = subject.convert(proc { read_template('statements2.html.cut') }, Cutaneous::FirstPassSyntax)
      result.must_equal read_template('statements1.html.cut')
    end
  end
end

describe Cutaneous do
  let(:template_root) { File.expand_path("../fixtures", __FILE__)                     }
  let(:engine)        { engine1 }
  let(:engine1)        { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }
  let(:engine2)        { Cutaneous::Engine.new(template_root, Cutaneous::SecondPassSyntax, "html") }

  it "Allows you to include other templates and pass them parameters" do
    context = ContextHash(right: "right")
    result = engine.render("include", context)
    result.must_equal "right = right\nright = wrong\nright = left\n"
  end

  it "Honors the format parameter" do
    context = ContextHash(right: "right")
    result = engine.render("include", context, "rss")
    result.must_equal "right = rss\nwrong = rss\nleft = rss\n"
  end

  it "Passes instance variables onto includes" do
    context = ContextHash(right: "right")
    result = engine.render("instance", context)
    result.must_equal "left = wrong\n"
  end

  it "Allows you to render a template string" do
    context = ContextHash(right: "left")
    result = engine.render_string("${ right }", context)
    result.must_equal "left"
  end

  it "Lets you mix template tags" do
    context = ContextHash(right: "left")
    result = engine.render_string("${ right } = {{ result }}", context)
    result.must_equal "left = {{ result }}"
  end

  it "Has a configurable lexer class" do
    engine = Cutaneous::Engine.new(template_root, Cutaneous::SecondPassSyntax)
    context = ContextHash(right: "wrong")
    result = engine.render_string("${ left } = {{ right }}", context)
    result.must_equal "${ left } = wrong"
  end

  it "Allows for multiple template roots" do
    roots = Array(template_root)
    roots.push File.join(template_root, "other")
    engine = Cutaneous::Engine.new(roots, Cutaneous::FirstPassSyntax)
    context = ContextHash(right: "wrong")
    result = engine.render_string('%{ include "different" }', context)
    result.must_equal "wrong\n"
  end

  it "Throws a resonable error if asked to include a non-existant file" do
    context = ContextHash(right: "wrong")
    test = proc { engine.render_string('%{ include "different" }', context) }
    test.must_raise Cutaneous::UnknownTemplateError
  end

  it "Maintains source line numbers for exceptions" do
    context = ContextHash(right: "wrong")
    test = proc { engine.render("error", context) }
    test.must_raise RuntimeError
    backtrace = message = nil
    begin
      test.call
    rescue RuntimeError => e
      message   = e.message
      backtrace = e.backtrace
    end
    filename, line = backtrace.first.split(":")
    filename.must_equal File.join(template_root, "error.html.cut")
    line.must_equal message
  end

  it "Maintains source line numbers for exceptions raised by includes" do
    context = ContextHash(right: "wrong")
    test = proc { engine.render("included_error", context) }
    test.must_raise RuntimeError
    backtrace = message = nil
    begin
      test.call
    rescue RuntimeError => e
      message   = e.message
      backtrace = e.backtrace
    end
    filename, line = backtrace.first.split(":")
    filename.must_equal File.join(template_root, "other/error.html.cut")
    line.must_equal message
  end

  it "Renders proc instances as strings" do
    context = ContextHash(right: "wrong")
    template = proc { "right" }
    result = engine.render(template, context, "rss")
    result.must_equal "right"
  end

  it "calls #close on any IO instances passed to it" do
    context = ContextHash(right: "correct")
    template = MiniTest::Mock.new
    template.expect(:close, nil)
    template.expect(:read, "${ right }")
    template.expect(:path, "/path/to/template.html.cut")
    result = engine.render(template, context, "rss")
    template.verify
    result.must_equal "correct"
  end

  it "Allows for configuration of the engine's default format" do
    engine.default_format = "rss"
    context = ContextHash(right: "right")
    result = engine.render("include", context)
    result.must_equal "right = rss\nwrong = rss\nleft = rss\n"
  end

  it "Accepts absolute template paths" do
    context = ContextHash(right: "right", code: "<tag/>")
    result = engine.render(File.join(template_root, "expressions1"), context)
    result.must_equal "This is right &lt;tag/&gt;\n"
  end

  it "Tests for the existence of a template file for a certain format" do
    assert engine.template_exists?("expressions1", "html")
    assert engine.template_exists?("other/error", "html")
    assert engine.template_exists?("include", "rss")
    refute engine.template_exists?("missing", "rss")
  end

  it "returns the full path to a template if found" do
    assert_equal "#{template_root}/expressions1", engine.template_location("expressions1", "html")
    assert_equal nil, engine.template_location("missing", "rss")
  end

  it "can determine if a template contains tags for its syntax" do
    assert engine.dynamic_template?("i am ${dynamic}")
    refute engine.dynamic_template?("i am not dynamic")
  end

   it "ignores escaped tags when testing for dynamic status" do
     refute engine.dynamic_template?("i am not \\${dynamic}")
   end

  it "Passes any instance variables & locals between contexts" do
    context = ContextHash(right: "left")
    result1 = engine.render("instance", context)
    context = ContextHash({}, context)
    result2 = engine.render("instance", context)
    result2.must_equal result1
  end

  it "Silently discards missing variables" do
    context = ContextHash(right: "left")
    result1 = engine.render("missing", context)
    result1.must_equal "missing: \n"
  end

  it "Overwrites object methods with parameters" do
    klass = Class.new(Object) do
      def monkey; "see"; end
      def to_s  ; "object"; end
    end
    context = TestContext.new(klass.new)
    result = engine.render_string("${ monkey } ${ to_s }", context)
    result.must_equal "see object"
    context = TestContext.new(klass.new, monkey: "magic", to_s: "fairy")
    result = engine.render_string("${ monkey } ${ to_s }", context)
    result.must_equal "magic fairy"
  end

  it "Overwrites helper methods with local values" do
    context_class = Class.new(TestContext) do
      def monkey
        "wrong"
      end
    end
    context = context_class.new(Object.new, monkey: "magic", to_s: "fairy")
    # def context.monkey; "wrong"; end
    result = engine.render_string("${ monkey } ${ to_s }", context)
    result.must_equal "magic fairy"
  end

  it "Preserves the original context locals after includes" do
    context = ContextHash({})
    result = engine.render("locals/parent", context)
    result.must_equal "Child 1\nChild 2\n\n"
  end
end