require "tempfile"

require File.dirname(__FILE__) + "/../lib/doc_storage"

module DocStorage
  describe SimpleDocument do
    SIMPLE_FIXTURE_FILE = File.dirname(__FILE__) + "/fixtures/simple.txt"

    Spec::Matchers.define :load_as_document do |document|
      match do |string|
        SimpleDocument.load(string) == document
      end
    end

    before :each do
      @document = SimpleDocument.new({ "a" => 42, "b" => 43 }, "body")

      @document_without_headers_without_body = SimpleDocument.new({}, "")
      @document_without_headers_with_body = SimpleDocument.new({}, "line1\nline2")
      @document_with_headers_without_body = SimpleDocument.new(
        { "a" => "42", "b" => "43" },
        ""
      )
      @document_with_headers_with_body = SimpleDocument.new(
        { "a" => "42", "b" => "43" },
        "line1\nline2"
      )

      @document_with_ugly_header = SimpleDocument.new(
        { "a" => "\xFF\377\0\a\b\t\n\v\f\r\"'\\\xFF\377\0\a\b\t\n\v\f\r\"'\\" },
        ""
      )
      @document_with_invalid_header = SimpleDocument.new(
        { "in\nvalid" => "42" },
        ""
      )
    end

    describe "initialize" do
      it "sets attributes correctly" do
        @document.headers.should == {"a" => 42, "b" => 43}
        @document.body.should == "body"
      end
    end

    describe "==" do
      it "returns true when passed the same object" do
        @document.should == @document
      end

      it "returns true when passed a SimpleDocument initialized with the same parameters" do
        @document.should == SimpleDocument.new({"a" => 42, "b" => 43}, "body")
      end

      it "returns false when passed some random object" do
        @document.should_not == Object.new
      end

      it "returns false when passed a subclass of SimpleDocument initialized with the same parameters" do
        class SubclassedSimpleDocument < SimpleDocument
        end

        @document.should_not ==
          SubclassedSimpleDocument.new({"a" => 42, "b" => 43}, "body")
      end

      it "returns false when passed a SimpleDocument initialized with different parameters" do
        @document.should_not == SimpleDocument.new({ "a" => 44, "b" => 45 }, "body")
        @document.should_not == SimpleDocument.new({ "a" => 42, "b" => 43 }, "nobody")
      end
    end

    describe "load" do
      it "loads document with no headers and no body" do
        "\n".should load_as_document(@document_without_headers_without_body)
      end

      it "loads document with no headers and body" do
        "\nline1\nline2".should load_as_document(
          @document_without_headers_with_body
        )
      end

      it "loads document with headers and no body" do
        "a: 42\nb: 43\n\n".should load_as_document(
          @document_with_headers_without_body
        )
      end

      it "loads document with headers and body" do
        "a: 42\nb: 43\n\nline1\nline2".should load_as_document(
          @document_with_headers_with_body
        )
      end

      it "loads document with no whitespace after the colon in headers" do
        "a:42\nb:43\n\n".should load_as_document(
          @document_with_headers_without_body
        )
      end

      it "loads document with multiple whitespace after the colon in headers" do
        "a: \t 42\nb: \t 43\n\n".should load_as_document(
          @document_with_headers_without_body
        )

      end

      it "loads document with multiple whitespace after the value in headers" do
        "a:42 \t \nb:43 \t \n\n".should load_as_document(
          @document_with_headers_without_body
        )
      end

      it "loads document with quoted header value" do
        "a: \"42\"\nb: \"43\"\n\n".should load_as_document(
          @document_with_headers_without_body
        )
        "a: '42'\nb: '43'\n\n".should load_as_document(
          @document_with_headers_without_body
        )

        "a: \"\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\"\n\n".should load_as_document(
          @document_with_ugly_header
        )
        "a: '\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\'\n\n".should load_as_document(
          @document_with_ugly_header
        )
      end

      it "does not load document with unterminated header value" do
        lambda {
          SimpleDocument.load("a: \"42\n\n")
        }.should raise_error(SyntaxError, "Unterminated header value: \"\\\"42\".")
        lambda {
          SimpleDocument.load("a: '42\n\n")
        }.should raise_error(SyntaxError, "Unterminated header value: \"'42\".")
      end

      it "does not load document with badly quoted header value" do
        lambda {
          SimpleDocument.load("a: \"4\"2\"\n\n")
        }.should raise_error(SyntaxError, "Badly quoted header value: \"\\\"4\\\"2\\\"\".")
        lambda {
          SimpleDocument.load("a: '4'2'\n\n")
        }.should raise_error(SyntaxError, "Badly quoted header value: \"'4'2'\".")
      end

      it "does not load document with quoted header value containing invalid escape sequence" do
        lambda {
          SimpleDocument.load("a: \"4\\z2\"\n\n")
        }.should raise_error(SyntaxError, "Invalid escape sequence in header value: \"\\\"4\\\\z2\\\"\".")
        lambda {
          SimpleDocument.load("a: '4\\z2'\n\n")
        }.should raise_error(SyntaxError, "Invalid escape sequence in header value: \"'4\\\\z2'\".")
      end

      it "does not load document with invalid headers" do
        lambda {
          SimpleDocument.load("bull\tshit\n")
        }.should raise_error(SyntaxError, "Invalid header: \"bull\\tshit\".")
      end

      it "does not load document with unterminated headers" do
        lambda {
          SimpleDocument.load("a: 42\nb: 42\n")
        }.should raise_error(SyntaxError, "Unterminated headers.")
      end

      it "loads document from IO-like object" do
        StringIO.open("a: 42\nb: 43\n\nline1\nline2") do |io|
          SimpleDocument.load(io).should == @document_with_headers_with_body
        end
      end

      it "loads document when detecting a boundary" do
        SimpleDocument.load(
          "a: 42\nb: 43\nBoundary: =====\n\nline1\nline2\n--=====\nbullshit",
          :detect
        ).should == SimpleDocument.new(
          { "a" => "42", "b" => "43", "Boundary" => "=====" },
          "line1\nline2"
        )
      end

      it "does not load document when detecting a boundary and no boundary defined" do
        lambda {
          SimpleDocument.load(
            "a: 42\nb: 43\n\nline1\nline2\n--=====\nbullshit",
            :detect
          )
        }.should raise_error(SyntaxError, "No boundary defined.")
      end

      it "loads document when passed a boundary" do
        SimpleDocument.load(
          "a: 42\nb: 43\n\nline1\nline2\n--=====\nbullshit",
          "====="
        ).should == @document_with_headers_with_body
      end
    end

    describe "load_file" do
      it "loads document" do
        SimpleDocument.load_file(SIMPLE_FIXTURE_FILE).should ==
          @document_with_headers_with_body
      end
    end

    describe "to_s" do
      it "serializes document with no headers and no body" do
        @document_without_headers_without_body.to_s.should == "\n"
      end

      it "serializes document with no headers and body" do
        @document_without_headers_with_body.to_s.should == "\nline1\nline2"
      end

      it "serializes document with headers and no body" do
        @document_with_headers_without_body.to_s.should == "a: 42\nb: 43\n\n"
      end

      it "serializes document with headers and body" do
        @document_with_headers_with_body.to_s.should ==
          "a: 42\nb: 43\n\nline1\nline2"
      end

      it "serializes document with ugly header" do
        @document_with_ugly_header.to_s.should ==
          "a: \"\\377\\377\\000\\a\\b\\t\\n\\v\\f\\r\\\"'\\\\\\377\\377\\000\\a\\b\\t\\n\\v\\f\\r\\\"'\\\\\"\n\n"
      end

      it "does not serialize document with invalid header name" do
        lambda {
          @document_with_invalid_header.to_s
        }.should raise_error(SyntaxError, "Invalid header name: \"in\\nvalid\".")
      end
    end

    describe "save" do
      it "saves document" do
        StringIO.open("", "w") do |io|
          @document_with_headers_with_body.save(io)
          io.string.should == "a: 42\nb: 43\n\nline1\nline2"
        end
      end
    end

    describe "save_file" do
      it "saves document" do
        # The "ensure" blocks aren't really necessary -- the tempfile will be
        # closed and unlinked upon its object destruction automatically. However
        # I think that being explicit and deterministic doesn't hurt.

        begin
          tempfile = Tempfile.new("doc_storage")
          tempfile.close

          @document_with_headers_with_body.save_file(tempfile.path)

          tempfile.open
          begin
            tempfile.read.should == "a: 42\nb: 43\n\nline1\nline2"
          ensure
            tempfile.close
          end
        ensure
          tempfile.unlink
        end
      end
    end
  end
end