Update tests

Update tests

defmodule Xema.RefTest do
  use ExUnit.Case, async: true

  doctest Xema.Ref

  import Xema, only: [valid?: 2, validate: 2]

  alias Xema.Ref
  alias Xema.Schema
  alias Xema.SchemaError

  describe "schema with ref root pointer" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            properties: %{
              foo: {:ref, "#"}
            },
            additional_properties: false
          })
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 1}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{bar: 1}) ==
               {:error, %{properties: %{bar: %{additional_properties: false}}}}
    end

    test "validate/2 with recursive valid data", %{schema: schema} do
      assert validate(schema, %{foo: %{foo: %{foo: 3}}}) == :ok
    end

    test "validate/2 with recursive invalid data", %{schema: schema} do
      assert validate(schema, %{foo: %{foo: %{bar: 3}}}) ==
               {:error,
                %{
                  properties: %{
                    foo: %{
                      properties: %{
                        foo: %{
                          properties: %{bar: %{additional_properties: false}}
                        }
                      }
                    }
                  }
                }}
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{schema: schema} do
      ref = Ref.new("#")
      assert schema = Ref.fetch!(ref, schema, nil)
    end

    test "Ref.fetch!/3 returns nil for an invalid ref", %{schema: schema} do
      ref = Ref.new("#/foo")
      assert_raise KeyError, fn -> Ref.fetch!(ref, schema, nil) == nil end
    end
  end

  describe "schema with ref root-id pointer" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            id: "http://foo.com",
            properties: %{
              foo: {:ref, "http://foo.com"}
            },
            additional_properties: false
          })
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 1}) == :ok
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{schema: schema} do
      ref = Ref.new("", URI.parse("http://foo.com"))
      assert schema = Ref.fetch!(ref, schema, nil)
    end

    test "Ref.fetch!/3 returns nil for an invalid ref", %{schema: schema} do
      ref = Ref.new("#/foo")
      assert_raise KeyError, fn -> Ref.fetch!(ref, schema, nil) == nil end
    end
  end

  describe "schema with a ref to property" do
    setup do
      data = [
        properties: %{
          foo: :integer,
          bar: {:ref, "#/properties/foo"},
          baz: {:ref, "#/properties/foo"}
        },
        additional_properties: false
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 42}) == :ok
      assert validate(schema, %{bar: 42}) == :ok
      assert validate(schema, %{foo: 21, bar: 42}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{foo: "42"}) ==
               {:error, %{properties: %{foo: %{type: :integer, value: "42"}}}}

      assert validate(schema, %{bar: "42"}) ==
               {:error, %{properties: %{bar: %{type: :integer, value: "42"}}}}

      assert validate(schema, %{foo: "21", bar: "42"}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{type: :integer, value: "42"},
                    foo: %{type: :integer, value: "21"}
                  }
                }}
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{non_inline: schema} do
      ref = Ref.new("#/properties/foo")
      assert {%Schema{type: :integer}, ^schema} = Ref.fetch!(ref, schema, nil)
    end
  end

  describe "schema with a ref to property (string key)" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              "foo" => :integer,
              "bar" => {:ref, "#/properties/foo"}
            },
            additional_properties: false
          )
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{"foo" => 42}) == :ok
      assert validate(schema, %{"bar" => 42}) == :ok
      assert validate(schema, %{"foo" => 21, "bar" => 42}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{"foo" => "42"}) ==
               {:error,
                %{properties: %{"foo" => %{type: :integer, value: "42"}}}}

      assert validate(schema, %{"bar" => "42"}) ==
               {:error,
                %{properties: %{"bar" => %{type: :integer, value: "42"}}}}

      assert validate(schema, %{"foo" => "21", "bar" => "42"}) ==
               {:error,
                %{
                  properties: %{
                    "bar" => %{type: :integer, value: "42"},
                    "foo" => %{type: :integer, value: "21"}
                  }
                }}
    end
  end

  describe "ref ignores any sibling" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              reffed: :list
            },
            properties: %{
              foo: [
                max_items: 2,
                ref: "#/definitions/reffed"
              ]
            }
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema ==
               %Xema{
                 refs: %{},
                 schema: %Xema.Schema{
                   definitions: %{reffed: %Xema.Schema{type: :list}},
                   properties: %{foo: %Xema.Schema{type: :list}}
                 }
               }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, %{foo: []})
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, %{foo: 1})
    end

    test "with valid value ignoring max items", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3, 4])
    end
  end

  describe "ref to list (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            {:list,
             items: [
               :integer,
               {:ref, "#/items/0"},
               {:ref, "#/items/1"}
             ]},
            inline: false
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{
                 "#/items/0" => %Xema.Schema{type: :integer},
                 "#/items/1" => %Xema.Schema{
                   ref: %Xema.Ref{pointer: "#/items/0"}
                 }
               },
               schema: %Xema.Schema{
                 items: [
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{ref: %Xema.Ref{pointer: "#/items/0"}},
                   %Xema.Schema{ref: %Xema.Ref{pointer: "#/items/1"}}
                 ],
                 type: :list
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3])
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, ["1", 2, 3])
      refute Xema.valid?(schema, [1, "2", 3])
      refute Xema.valid?(schema, [1, 2, "3"])
    end
  end

  describe "ref to list" do
    setup do
      %{
        schema:
          Xema.new(
            {:list,
             items: [
               :integer,
               {:ref, "#/items/0"},
               {:ref, "#/items/1"}
             ]}
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{},
               schema: %Xema.Schema{
                 items: [
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{type: :integer}
                 ],
                 type: :list
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3])
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, ["1", 2, 3])
      refute Xema.valid?(schema, [1, "2", 3])
      refute Xema.valid?(schema, [1, 2, "3"])
    end
  end

  describe "ref ignores any sibling (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            [
              definitions: %{
                reffed: :list
              },
              properties: %{
                foo: [
                  max_items: 2,
                  ref: "#/definitions/reffed"
                ]
              }
            ],
            inline: false
          )
      }
    end

    test ": check schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{"#/definitions/reffed" => %Xema.Schema{type: :list}},
               schema: %Xema.Schema{
                 definitions: %{reffed: %Xema.Schema{type: :list}},
                 properties: %{
                   foo: %Xema.Schema{
                     max_items: 2,
                     ref: %Xema.Ref{pointer: "#/definitions/reffed"}
                   }
                 }
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, %{foo: []})
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, %{foo: 1})
    end

    test "with valid value ignoring max items", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3, 4])
    end
  end

  describe "schema with ref and definitions" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              foo: {:ref, "#/definitions/pos"},
              bar: {:ref, "#/definitions/neg"}
            },
            definitions: %{
              pos: {:integer, minimum: 0},
              neg: {:integer, maximum: 0}
            }
          )
      }
    end

    test "validate/2 with valid values", %{schema: schema} do
      assert validate(schema, %{foo: 5, bar: -1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{foo: -1, bar: 1}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{maximum: 0, value: 1},
                    foo: %{minimum: 0, value: -1}
                  }
                }}
    end
  end

  describe "schema with ref to custom data" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              foo: {:ref, "#/abc/def/pos"},
              bar: {:ref, "#/abc/neg"}
            },
            abc: %{
              def: %{
                pos: {:integer, minimum: 0}
              },
              neg: {:integer, maximum: 0}
            }
          )
      }
    end

    test "validate/2 with valid values", %{schema: schema} do
      assert validate(schema, %{foo: 5, bar: -1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{foo: -1, bar: 1}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{maximum: 0, value: 1},
                    foo: %{minimum: 0, value: -1}
                  }
                }}
    end
  end

  describe "schema with ref chain" do
    setup do
      data = [
        properties: %{
          foo: {:ref, "#/definitions/bar"}
        },
        definitions: %{
          bar: {:ref, "#/definitions/pos"},
          pos: {:integer, minimum: 0}
        }
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, %{foo: 42}) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, %{foo: -21}) ==
               {:error, %{properties: %{foo: %{minimum: 0, value: -21}}}}
    end

    test "Ref.fetch!/3 return schema for a valid ref", %{non_inline: schema} do
      ref = Ref.new("#/definitions/bar")

      assert {ref_schema, ^schema} = Ref.fetch!(ref, schema, nil)

      assert ref_schema == %Schema{
               ref: %Ref{pointer: "#/definitions/pos"}
             }

      ref = Ref.new("#/definitions/pos")

      assert {ref_schema, ^schema} = Ref.fetch!(ref, schema, nil)

      assert ref_schema == %Schema{
               type: :integer,
               minimum: 0
             }
    end
  end

  describe "schema with ref as keyword" do
    setup do
      %{
        schema:
          Xema.new(
            ref: "#/definitions/pos",
            definitions: %{
              pos: {:integer, minimum: 0}
            }
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, 42) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, -42) == {:error, %{minimum: 0, value: -42}}
    end
  end

  describe "schema with ref to id" do
    setup do
      %{
        schema:
          Xema.new(
            id: "http://foo.com",
            ref: "pos",
            definitions: %{
              pos: {:integer, minimum: 0, id: "http://foo.com/pos"}
            }
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, 42) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, -42) == {:error, %{minimum: 0, value: -42}}
    end
  end

  describe "schema with ref to a list item" do
    setup do
      %{
        schema:
          Xema.new(
            items: [
              :integer,
              {:ref, "#/items/0"},
              {:ref, "#/items/1"}
            ]
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, [1, 2]) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, [1, "2"]) ==
               {:error, %{items: [{1, %{type: :integer, value: "2"}}]}}
    end
  end

  describe "schema with ref for additional items" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              str: :string
            },
            items: [:integer],
            additional_items: {:ref, "#/definitions/str"}
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, [1, "foo"]) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, [1, 2]) ==
               {:error, %{items: [{1, %{type: :string, value: 2}}]}}
    end
  end

  describe "schema with escaped refs" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              "tilda~field": :integer,
              "slash/field": :integer,
              "percent%field": :integer
            },
            properties: %{
              tilda_1: {:ref, "#/definitions/tilda~field"},
              tilda_2: {:ref, "#/definitions/tilda~0field"},
              tilda_3: {:ref, "#/definitions/tilda%7Efield"},
              percent: {:ref, "#/definitions/percent%25field"},
              slash_1: {:ref, "#/definitions/slash~1field"},
              slash_2: {:ref, "#/definitions/slash%2Ffield"}
            }
          )
      }
    end

    test "validate/2 tilda_1 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_1: 1}) == :ok
    end

    test "validate/2 tilda_1 with invalid value", %{schema: schema} do
      assert validate(schema, %{tilda_1: "1"}) ==
               {:error,
                %{properties: %{tilda_1: %{type: :integer, value: "1"}}}}
    end

    test "validate/2 tilda_2 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_2: 1}) == :ok
    end

    test "validate/2 tilda_3 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_3: 1}) == :ok
    end

    test "validate/2 percent with valid value", %{schema: schema} do
      assert validate(schema, %{percent: 1}) == :ok
    end

    test "validate/2 slash_1 with valid value", %{schema: schema} do
      assert validate(schema, %{slash_1: 1}) == :ok
    end

    test "validate/2 slash_2 with valid value", %{schema: schema} do
      assert validate(schema, %{slash_2: 1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{
               tilda_1: "1",
               tilda_2: "1",
               tilda_3: "1",
               slash_1: "1",
               slash_2: "1",
               percent: "1"
             }) ==
               {:error,
                %{
                  properties: %{
                    percent: %{type: :integer, value: "1"},
                    slash_1: %{type: :integer, value: "1"},
                    slash_2: %{type: :integer, value: "1"},
                    tilda_1: %{type: :integer, value: "1"},
                    tilda_2: %{type: :integer, value: "1"},
                    tilda_3: %{type: :integer, value: "1"}
                  }
                }}
    end
  end

  describe "schema with recursive refs" do
    setup do
      %{
        schema:
          Xema.new({
            :map,
            id: "http://localhost:1234/tree",
            description: "tree of nodes",
            properties: %{
              meta: :string,
              nodes: {:list, items: {:ref, "node"}}
            },
            required: [:meta, :nodes],
            definitions: %{
              node:
                {:map,
                 id: "http://localhost:1234/node",
                 description: "node",
                 properties: %{
                   value: :number,
                   subtree: {:ref, "tree"}
                 },
                 required: [:value]}
            }
          })
      }
    end

    test "validate/2 with a valid root", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: []
      }

      assert validate(schema, tree) == :ok
    end

    test "refs", %{schema: schema} do
      assert Map.keys(schema.refs) == [
               "http://localhost:1234/node",
               "http://localhost:1234/tree"
             ]
    end

    test "validate/2 with a valid tree", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: [
          %{
            value: 5,
            subtree: %{
              meta: "sub",
              nodes: [
                %{
                  value: 42
                },
                %{
                  value: 21,
                  subtree: %{
                    meta: "foo",
                    value: 667,
                    nodes: []
                  }
                }
              ]
            }
          }
        ]
      }

      assert validate(schema, tree) == :ok
    end

    test "validate/2 with a missing nodes property", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: [
          %{
            value: 5,
            subtree: %{
              meta: "sub",
              nodes: [
                %{
                  value: 42
                },
                %{
                  value: 21,
                  subtree: %{
                    meta: "foo",
                    value: 667
                  }
                }
              ]
            }
          }
        ]
      }

      assert validate(schema, tree) ==
               {:error,
                %{
                  properties: %{
                    nodes: %{
                      items: [
                        {0,
                         %{
                           properties: %{
                             subtree: %{
                               properties: %{
                                 nodes: %{
                                   items: [
                                     {1,
                                      %{
                                        properties: %{
                                          subtree: %{required: [:nodes]}
                                        }
                                      }}
                                   ]
                                 }
                               }
                             }
                           }
                         }}
                      ]
                    }
                  }
                }}
    end
  end

  describe "location-independent identifier" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            definitions: %{
              foo: {:integer, id: "#num"}
            },
            ref: "#num"
          })
      }
    end

    test "with valid data", %{schema: schema} do
      assert validate(schema, 1) == :ok
    end
  end

  describe "escaped pointer ref" do
    setup do
      %{
        schema:
          Xema.new([
            {:properties,
             %{
               percent: {:ref, "#/percent%25field"},
               slash: {:ref, "#/slash~1field"},
               tilda: {:ref, "#/tilda~0field"}
             }},
            "percent%field": :integer,
            "slash/field": :integer,
            "tilda~field": :integer
          ])
      }
    end

    test "slash invalid", %{schema: schema} do
      data = %{slash: "aoeu"}
      refute valid?(schema, data)
    end

    test "tilda invalid", %{schema: schema} do
      data = %{tilda: "aoeu"}
      refute valid?(schema, data)
    end

    test "percent invalid", %{schema: schema} do
      data = %{percent: "aoeu"}
      refute valid?(schema, data)
    end

    test "slash valid", %{schema: schema} do
      data = %{slash: 123}
      assert valid?(schema, data)
    end

    test "tilda valid", %{schema: schema} do
      data = %{tilda: 123}
      assert valid?(schema, data)
    end

    test "percent valid", %{schema: schema} do
      data = %{percent: 123}
      assert valid?(schema, data)
    end
  end

  describe "build schema" do
    test "with invalid ref" do
      msg = "Ref #/foo not found."

      assert_raise SchemaError, msg, fn ->
        Xema.new({:ref, "#/foo"})
      end
    end
    test "(non-inline) with invalid ref" do
      msg = "Ref #/foo not found."

      assert_raise SchemaError, msg, fn ->
        Xema.new({:ref, "#/foo"}, inline: false)
      end
    end
    end

  describe "non circular ref inside a circular ref" do
    setup do
      data = [
        definitions: %{
          non_neg: {:integer, minimum: 0},
          item:
            {:map,
             properties: %{
               next: {:ref, "#/definitions/item"},
               num: {:ref, "#/definitions/non_neg"}
             }}
        },
        properties: %{
          nums: {:ref, "#/definitions/item"}
        }
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "valid?/2 with non-inline", %{non_inline: schema} do
      assert valid?(schema, %{nums: %{num: 1, next: %{num: 2}}})
    end

    test "valid?/2 with schema", %{schema: schema} do
      assert valid?(schema, %{nums: %{num: 1, next: %{num: 2}}})
    end
  end

  describe "refs in pattern properties" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              commands:
                {:map,
                 pattern_properties: %{
                   ".*_cmd" => {:ref, "#/definitions/command"}
                 }},
              command:
                {:map,
                 properties: %{
                   os: :string,
                   cmd: :string
                 }}
            },
            properties: %{
              commands: {:ref, "#/definitions/commands"}
            }
          )
      }
    end

    test "with valid data", %{schema: schema} do
      assert valid?(schema, %{commands: %{"foo_cmd" => %{os: "mac"}}})
    end
  end

  describe "refs in pattern properties (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            [
              definitions: %{
                commands:
                  {:map,
                   pattern_properties: %{
                     ".*_cmd" => {:ref, "#/definitions/command"}
                   }},
                command:
                  {:map,
                   properties: %{
                     os: :string,
                     cmd: :string
                   }}
              },
              properties: %{
                commands: {:ref, "#/definitions/commands"}
              }
            ],
            inline: false
          )
      }
    end

    @tag :only
    test "with valid data", %{schema: schema} do
      assert valid?(schema, %{commands: %{"foo_cmd" => %{os: "mac"}}})
    end
  end
end

defmodule Xema.RefTest do
  use ExUnit.Case, async: true

  doctest Xema.Ref

  import Xema, only: [valid?: 2, validate: 2]

  alias Xema.Ref
  alias Xema.Schema
  alias Xema.SchemaError

  describe "schema with ref root pointer" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            properties: %{
              foo: {:ref, "#"}
            },
            additional_properties: false
          })
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 1}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{bar: 1}) ==
               {:error, %{properties: %{bar: %{additional_properties: false}}}}
    end

    test "validate/2 with recursive valid data", %{schema: schema} do
      assert validate(schema, %{foo: %{foo: %{foo: 3}}}) == :ok
    end

    test "validate/2 with recursive invalid data", %{schema: schema} do
      assert validate(schema, %{foo: %{foo: %{bar: 3}}}) ==
               {:error,
                %{
                  properties: %{
                    foo: %{
                      properties: %{
                        foo: %{
                          properties: %{bar: %{additional_properties: false}}
                        }
                      }
                    }
                  }
                }}
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{schema: schema} do
      ref = Ref.new("#")
      assert schema = Ref.fetch!(ref, schema, nil)
    end

    test "Ref.fetch!/3 returns nil for an invalid ref", %{schema: schema} do
      ref = Ref.new("#/foo")
      assert_raise KeyError, fn -> Ref.fetch!(ref, schema, nil) == nil end
    end
  end

  describe "schema with ref root-id pointer" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            id: "http://foo.com",
            properties: %{
              foo: {:ref, "http://foo.com"}
            },
            additional_properties: false
          })
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 1}) == :ok
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{schema: schema} do
      ref = Ref.new("", URI.parse("http://foo.com"))
      assert schema = Ref.fetch!(ref, schema, nil)
    end

    test "Ref.fetch!/3 returns nil for an invalid ref", %{schema: schema} do
      ref = Ref.new("#/foo")
      assert_raise KeyError, fn -> Ref.fetch!(ref, schema, nil) == nil end
    end
  end

  describe "schema with a ref to property" do
    setup do
      data = [
        properties: %{
          foo: :integer,
          bar: {:ref, "#/properties/foo"},
          baz: {:ref, "#/properties/foo"}
        },
        additional_properties: false
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{foo: 42}) == :ok
      assert validate(schema, %{bar: 42}) == :ok
      assert validate(schema, %{foo: 21, bar: 42}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{foo: "42"}) ==
               {:error, %{properties: %{foo: %{type: :integer, value: "42"}}}}

      assert validate(schema, %{bar: "42"}) ==
               {:error, %{properties: %{bar: %{type: :integer, value: "42"}}}}

      assert validate(schema, %{foo: "21", bar: "42"}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{type: :integer, value: "42"},
                    foo: %{type: :integer, value: "21"}
                  }
                }}
    end

    test "Ref.fetch!/3 returns schema for a valid ref", %{non_inline: schema} do
      ref = Ref.new("#/properties/foo")
      assert {%Schema{type: :integer}, ^schema} = Ref.fetch!(ref, schema, nil)
    end
  end

  describe "schema with a ref to property (string key)" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              "foo" => :integer,
              "bar" => {:ref, "#/properties/foo"}
            },
            additional_properties: false
          )
      }
    end

    test "validate/2 with valid data", %{schema: schema} do
      assert validate(schema, %{"foo" => 42}) == :ok
      assert validate(schema, %{"bar" => 42}) == :ok
      assert validate(schema, %{"foo" => 21, "bar" => 42}) == :ok
    end

    test "validate/2 with invalid data", %{schema: schema} do
      assert validate(schema, %{"foo" => "42"}) ==
               {:error,
                %{properties: %{"foo" => %{type: :integer, value: "42"}}}}

      assert validate(schema, %{"bar" => "42"}) ==
               {:error,
                %{properties: %{"bar" => %{type: :integer, value: "42"}}}}

      assert validate(schema, %{"foo" => "21", "bar" => "42"}) ==
               {:error,
                %{
                  properties: %{
                    "bar" => %{type: :integer, value: "42"},
                    "foo" => %{type: :integer, value: "21"}
                  }
                }}
    end
  end

  describe "ref ignores any sibling" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              reffed: :list
            },
            properties: %{
              foo: [
                max_items: 2,
                ref: "#/definitions/reffed"
              ]
            }
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema ==
               %Xema{
                 refs: %{},
                 schema: %Xema.Schema{
                   definitions: %{reffed: %Xema.Schema{type: :list}},
                   properties: %{foo: %Xema.Schema{type: :list}}
                 }
               }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, %{foo: []})
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, %{foo: 1})
    end

    test "with valid value ignoring max items", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3, 4])
    end
  end

  describe "ref to list (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            {:list,
             items: [
               :integer,
               {:ref, "#/items/0"},
               {:ref, "#/items/1"}
             ]},
            inline: false
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{
                 "#/items/0" => %Xema.Schema{type: :integer},
                 "#/items/1" => %Xema.Schema{
                   ref: %Xema.Ref{pointer: "#/items/0"}
                 }
               },
               schema: %Xema.Schema{
                 items: [
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{ref: %Xema.Ref{pointer: "#/items/0"}},
                   %Xema.Schema{ref: %Xema.Ref{pointer: "#/items/1"}}
                 ],
                 type: :list
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3])
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, ["1", 2, 3])
      refute Xema.valid?(schema, [1, "2", 3])
      refute Xema.valid?(schema, [1, 2, "3"])
    end
  end

  describe "ref to list" do
    setup do
      %{
        schema:
          Xema.new(
            {:list,
             items: [
               :integer,
               {:ref, "#/items/0"},
               {:ref, "#/items/1"}
             ]}
          )
      }
    end

    test "schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{},
               schema: %Xema.Schema{
                 items: [
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{type: :integer},
                   %Xema.Schema{type: :integer}
                 ],
                 type: :list
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3])
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, ["1", 2, 3])
      refute Xema.valid?(schema, [1, "2", 3])
      refute Xema.valid?(schema, [1, 2, "3"])
    end
  end

  describe "ref ignores any sibling (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            [
              definitions: %{
                reffed: :list
              },
              properties: %{
                foo: [
                  max_items: 2,
                  ref: "#/definitions/reffed"
                ]
              }
            ],
            inline: false
          )
      }
    end

    test ": check schema", %{schema: schema} do
      assert schema == %Xema{
               refs: %{"#/definitions/reffed" => %Xema.Schema{type: :list}},
               schema: %Xema.Schema{
                 definitions: %{reffed: %Xema.Schema{type: :list}},
                 properties: %{
                   foo: %Xema.Schema{
                     max_items: 2,
                     ref: %Xema.Ref{pointer: "#/definitions/reffed"}
                   }
                 }
               }
             }
    end

    test "with valid value", %{schema: schema} do
      assert Xema.valid?(schema, %{foo: []})
    end

    test "with invalid value", %{schema: schema} do
      refute Xema.valid?(schema, %{foo: 1})
    end

    test "with valid value ignoring max items", %{schema: schema} do
      assert Xema.valid?(schema, [1, 2, 3, 4])
    end
  end

  describe "schema with ref and definitions" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              foo: {:ref, "#/definitions/pos"},
              bar: {:ref, "#/definitions/neg"}
            },
            definitions: %{
              pos: {:integer, minimum: 0},
              neg: {:integer, maximum: 0}
            }
          )
      }
    end

    test "validate/2 with valid values", %{schema: schema} do
      assert validate(schema, %{foo: 5, bar: -1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{foo: -1, bar: 1}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{maximum: 0, value: 1},
                    foo: %{minimum: 0, value: -1}
                  }
                }}
    end
  end

  describe "schema with ref to custom data" do
    setup do
      %{
        schema:
          Xema.new(
            properties: %{
              foo: {:ref, "#/abc/def/pos"},
              bar: {:ref, "#/abc/neg"}
            },
            abc: %{
              def: %{
                pos: {:integer, minimum: 0}
              },
              neg: {:integer, maximum: 0}
            }
          )
      }
    end

    test "validate/2 with valid values", %{schema: schema} do
      assert validate(schema, %{foo: 5, bar: -1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{foo: -1, bar: 1}) ==
               {:error,
                %{
                  properties: %{
                    bar: %{maximum: 0, value: 1},
                    foo: %{minimum: 0, value: -1}
                  }
                }}
    end
  end

  describe "schema with ref chain" do
    setup do
      data = [
        properties: %{
          foo: {:ref, "#/definitions/bar"}
        },
        definitions: %{
          bar: {:ref, "#/definitions/pos"},
          pos: {:integer, minimum: 0}
        }
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, %{foo: 42}) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, %{foo: -21}) ==
               {:error, %{properties: %{foo: %{minimum: 0, value: -21}}}}
    end

    test "Ref.fetch!/3 return schema for a valid ref", %{non_inline: schema} do
      ref = Ref.new("#/definitions/bar")

      assert {ref_schema, ^schema} = Ref.fetch!(ref, schema, nil)

      assert ref_schema == %Schema{
               ref: %Ref{pointer: "#/definitions/pos"}
             }

      ref = Ref.new("#/definitions/pos")

      assert {ref_schema, ^schema} = Ref.fetch!(ref, schema, nil)

      assert ref_schema == %Schema{
               type: :integer,
               minimum: 0
             }
    end
  end

  describe "schema with ref as keyword" do
    setup do
      %{
        schema:
          Xema.new(
            ref: "#/definitions/pos",
            definitions: %{
              pos: {:integer, minimum: 0}
            }
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, 42) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, -42) == {:error, %{minimum: 0, value: -42}}
    end
  end

  describe "schema with ref to id" do
    setup do
      %{
        schema:
          Xema.new(
            id: "http://foo.com",
            ref: "pos",
            definitions: %{
              pos: {:integer, minimum: 0, id: "http://foo.com/pos"}
            }
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, 42) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, -42) == {:error, %{minimum: 0, value: -42}}
    end
  end

  describe "schema with ref to a list item" do
    setup do
      %{
        schema:
          Xema.new(
            items: [
              :integer,
              {:ref, "#/items/0"},
              {:ref, "#/items/1"}
            ]
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, [1, 2]) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, [1, "2"]) ==
               {:error, %{items: [{1, %{type: :integer, value: "2"}}]}}
    end
  end

  describe "schema with ref for additional items" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              str: :string
            },
            items: [:integer],
            additional_items: {:ref, "#/definitions/str"}
          )
      }
    end

    test "validate/2 with valid value", %{schema: schema} do
      assert validate(schema, [1, "foo"]) == :ok
    end

    test "validate/2 with invalid value", %{schema: schema} do
      assert validate(schema, [1, 2]) ==
               {:error, %{items: [{1, %{type: :string, value: 2}}]}}
    end
  end

  describe "schema with escaped refs" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              "tilda~field": :integer,
              "slash/field": :integer,
              "percent%field": :integer
            },
            properties: %{
              tilda_1: {:ref, "#/definitions/tilda~field"},
              tilda_2: {:ref, "#/definitions/tilda~0field"},
              tilda_3: {:ref, "#/definitions/tilda%7Efield"},
              percent: {:ref, "#/definitions/percent%25field"},
              slash_1: {:ref, "#/definitions/slash~1field"},
              slash_2: {:ref, "#/definitions/slash%2Ffield"}
            }
          )
      }
    end

    test "validate/2 tilda_1 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_1: 1}) == :ok
    end

    test "validate/2 tilda_1 with invalid value", %{schema: schema} do
      assert validate(schema, %{tilda_1: "1"}) ==
               {:error,
                %{properties: %{tilda_1: %{type: :integer, value: "1"}}}}
    end

    test "validate/2 tilda_2 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_2: 1}) == :ok
    end

    test "validate/2 tilda_3 with valid value", %{schema: schema} do
      assert validate(schema, %{tilda_3: 1}) == :ok
    end

    test "validate/2 percent with valid value", %{schema: schema} do
      assert validate(schema, %{percent: 1}) == :ok
    end

    test "validate/2 slash_1 with valid value", %{schema: schema} do
      assert validate(schema, %{slash_1: 1}) == :ok
    end

    test "validate/2 slash_2 with valid value", %{schema: schema} do
      assert validate(schema, %{slash_2: 1}) == :ok
    end

    test "validate/2 with invalid values", %{schema: schema} do
      assert validate(schema, %{
               tilda_1: "1",
               tilda_2: "1",
               tilda_3: "1",
               slash_1: "1",
               slash_2: "1",
               percent: "1"
             }) ==
               {:error,
                %{
                  properties: %{
                    percent: %{type: :integer, value: "1"},
                    slash_1: %{type: :integer, value: "1"},
                    slash_2: %{type: :integer, value: "1"},
                    tilda_1: %{type: :integer, value: "1"},
                    tilda_2: %{type: :integer, value: "1"},
                    tilda_3: %{type: :integer, value: "1"}
                  }
                }}
    end
  end

  describe "schema with recursive refs" do
    setup do
      %{
        schema:
          Xema.new({
            :map,
            id: "http://localhost:1234/tree",
            description: "tree of nodes",
            properties: %{
              meta: :string,
              nodes: {:list, items: {:ref, "node"}}
            },
            required: [:meta, :nodes],
            definitions: %{
              node:
                {:map,
                 id: "http://localhost:1234/node",
                 description: "node",
                 properties: %{
                   value: :number,
                   subtree: {:ref, "tree"}
                 },
                 required: [:value]}
            }
          })
      }
    end

    test "validate/2 with a valid root", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: []
      }

      assert validate(schema, tree) == :ok
    end

    test "refs", %{schema: schema} do
      assert Map.keys(schema.refs) == [
               "http://localhost:1234/node",
               "http://localhost:1234/tree"
             ]
    end

    test "validate/2 with a valid tree", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: [
          %{
            value: 5,
            subtree: %{
              meta: "sub",
              nodes: [
                %{
                  value: 42
                },
                %{
                  value: 21,
                  subtree: %{
                    meta: "foo",
                    value: 667,
                    nodes: []
                  }
                }
              ]
            }
          }
        ]
      }

      assert validate(schema, tree) == :ok
    end

    test "validate/2 with a missing nodes property", %{schema: schema} do
      tree = %{
        meta: "root",
        nodes: [
          %{
            value: 5,
            subtree: %{
              meta: "sub",
              nodes: [
                %{
                  value: 42
                },
                %{
                  value: 21,
                  subtree: %{
                    meta: "foo",
                    value: 667
                  }
                }
              ]
            }
          }
        ]
      }

      assert validate(schema, tree) ==
               {:error,
                %{
                  properties: %{
                    nodes: %{
                      items: [
                        {0,
                         %{
                           properties: %{
                             subtree: %{
                               properties: %{
                                 nodes: %{
                                   items: [
                                     {1,
                                      %{
                                        properties: %{
                                          subtree: %{required: [:nodes]}
                                        }
                                      }}
                                   ]
                                 }
                               }
                             }
                           }
                         }}
                      ]
                    }
                  }
                }}
    end
  end

  describe "location-independent identifier" do
    setup do
      %{
        schema:
          Xema.new({
            :any,
            definitions: %{
              foo: {:integer, id: "#num"}
            },
            ref: "#num"
          })
      }
    end

    test "with valid data", %{schema: schema} do
      assert validate(schema, 1) == :ok
    end
  end

  describe "escaped pointer ref" do
    setup do
      %{
        schema:
          Xema.new([
            {:properties,
             %{
               percent: {:ref, "#/percent%25field"},
               slash: {:ref, "#/slash~1field"},
               tilda: {:ref, "#/tilda~0field"}
             }},
            "percent%field": :integer,
            "slash/field": :integer,
            "tilda~field": :integer
          ])
      }
    end

    test "slash invalid", %{schema: schema} do
      data = %{slash: "aoeu"}
      refute valid?(schema, data)
    end

    test "tilda invalid", %{schema: schema} do
      data = %{tilda: "aoeu"}
      refute valid?(schema, data)
    end

    test "percent invalid", %{schema: schema} do
      data = %{percent: "aoeu"}
      refute valid?(schema, data)
    end

    test "slash valid", %{schema: schema} do
      data = %{slash: 123}
      assert valid?(schema, data)
    end

    test "tilda valid", %{schema: schema} do
      data = %{tilda: 123}
      assert valid?(schema, data)
    end

    test "percent valid", %{schema: schema} do
      data = %{percent: 123}
      assert valid?(schema, data)
    end
  end

  describe "build schema" do
    test "with invalid ref" do
      msg = "Ref #/foo not found."

      assert_raise SchemaError, msg, fn ->
        Xema.new({:ref, "#/foo"})
      end
    end
  end

  describe "non circular ref inside a circular ref" do
    setup do
      data = [
        definitions: %{
          non_neg: {:integer, minimum: 0},
          item:
            {:map,
             properties: %{
               next: {:ref, "#/definitions/item"},
               num: {:ref, "#/definitions/non_neg"}
             }}
        },
        properties: %{
          nums: {:ref, "#/definitions/item"}
        }
      ]

      %{
        schema: Xema.new(data),
        non_inline: Xema.new(data, inline: false)
      }
    end

    test "valid?/2 with non-inline", %{non_inline: schema} do
      assert valid?(schema, %{nums: %{num: 1, next: %{num: 2}}})
    end

    test "valid?/2 with schema", %{schema: schema} do
      assert valid?(schema, %{nums: %{num: 1, next: %{num: 2}}})
    end
  end

  describe "refs in pattern properties" do
    setup do
      %{
        schema:
          Xema.new(
            definitions: %{
              commands:
                {:map,
                 pattern_properties: %{
                   ".*_cmd" => {:ref, "#/definitions/command"}
                 }},
              command:
                {:map,
                 properties: %{
                   os: :string,
                   cmd: :string
                 }}
            },
            properties: %{
              commands: {:ref, "#/definitions/commands"}
            }
          )
      }
    end

    test "with valid data", %{schema: schema} do
      assert valid?(schema, %{commands: %{"foo_cmd" => %{os: "mac"}}})
    end
  end

  describe "refs in pattern properties (non-inline)" do
    setup do
      %{
        schema:
          Xema.new(
            [
              definitions: %{
                commands:
                  {:map,
                   pattern_properties: %{
                     ".*_cmd" => {:ref, "#/definitions/command"}
                   }},
                command:
                  {:map,
                   properties: %{
                     os: :string,
                     cmd: :string
                   }}
              },
              properties: %{
                commands: {:ref, "#/definitions/commands"}
              }
            ],
            inline: false
          )
      }
    end

    @tag :only
    test "with valid data", %{schema: schema} do
      assert valid?(schema, %{commands: %{"foo_cmd" => %{os: "mac"}}})
    end
  end
end


! beautify docs

! beautify docs

# vim: ft=elixir sw=2 ts=2
#
# state_machine_behavior.ex
# generic state machine behavior
#

defmodule StateMachine.Behavior do
  @doc """
  Updates the context to the new state and activates
  the associated functions from event.calls.
  """
  def fire(context, event) do
    %{context | state: event[:to]}
    |> activate(event)
  end

  @doc """
  Alternative api to fire an event, with lookup of the
  next state by event_name and the provided states description.
  """
  def fire(states, context, event_name) do
    event = states[context.state][event_name]
    fire(context, event)
  end

  @doc """
  Call the associated functions with updated context respectively.
  """
  def activate(context, event) do
    Enum.reduce(event[:calls] || [], context, &(&1.(&2)))
  end

end

# vim: ft=elixir sw=2 ts=2
#
# state_machine_behavior.ex
# generic state machine behavior
#

defmodule StateMachine.Behavior do
  @doc """
  Updates the context to the new state and activates
  the associated functions from event.calls
  """
  def fire(context, event) do
    %{context | state: event[:to]}
    |> activate(event)
  end

  @doc """
  Alternative api to fire an event, with lookup of the
  next state by event_name and the provided states description
  """
  def fire(states, context, event_name) do
    event = states[context.state][event_name]
    fire(context, event)
  end

  @doc """
  Call the associated functions with updated context respectively
  """
  def activate(context, event) do
    Enum.reduce(event[:calls] || [], context, &(&1.(&2)))
  end

end


Make maps difference recursive

Make maps difference recursive

defmodule ExUnit.Formatter do
  @moduledoc """
  This module holds helper functions related to formatting and contains
  documentation about the formatting protocol.

  Formatters are registered at the `ExUnit.EventManager` event manager and
  will be send events by the runner.

  The following events are possible:

    * `{:suite_started, opts}` -
      the suite has started with the specified options to the runner.

    * `{:suite_finished, run_us, load_us}` -
      the suite has finished. `run_us` and `load_us` are the run and load
      times in microseconds respectively.

    * `{:case_started, test_case}` -
      a test case has started. See `ExUnit.TestCase` for details.

    * `{:case_finished, test_case}` -
      a test case has finished. See `ExUnit.TestCase` for details.

    * `{:test_started, test_case}` -
      a test case has started. See `ExUnit.Test` for details.

    * `{:test_finished, test_case}` -
      a test case has finished. See `ExUnit.Test` for details.

  """

  @type id :: term
  @type test_case :: ExUnit.TestCase.t
  @type test :: ExUnit.Test.t
  @type run_us :: pos_integer
  @type load_us :: pos_integer | nil

  import Exception, only: [format_stacktrace_entry: 1]

  @label_padding   "      "
  @counter_padding "     "
  @inspect_padding @counter_padding <> @label_padding

  @doc """
  Formats time taken running the test suite.

  It receives the time spent running the tests and
  optionally the time spent loading the test suite.

  ## Examples

      iex> format_time(10000, nil)
      "Finished in 0.01 seconds"

      iex> format_time(10000, 20000)
      "Finished in 0.03 seconds (0.02s on load, 0.01s on tests)"

      iex> format_time(10000, 200000)
      "Finished in 0.2 seconds (0.2s on load, 0.01s on tests)"

  """
  @spec format_time(run_us, load_us) :: String.t
  def format_time(run_us, nil) do
    "Finished in #{run_us |> normalize_us |> format_us} seconds"
  end

  def format_time(run_us, load_us) do
    run_us  = run_us |> normalize_us
    load_us = load_us |> normalize_us

    total_us = run_us + load_us
    "Finished in #{format_us total_us} seconds (#{format_us load_us}s on load, #{format_us run_us}s on tests)"
  end

  defp normalize_us(us) do
    div(us, 10000)
  end

  defp format_us(us) do
    if us < 10 do
      "0.0#{us}"
    else
      us = div us, 10
      "#{div(us, 10)}.#{rem(us, 10)}"
    end
  end

  @doc """
  Formats filters used to constrain cases to be run.

  ## Examples

      iex> format_filters([run: true, slow: false], :include)
      "Including tags: [run: true, slow: false]"

  """
  @spec format_filters(Keyword.t, atom) :: String.t
  def format_filters(filters, type) do
    case type do
      :include -> "Including tags: #{inspect filters}"
      :exclude -> "Excluding tags: #{inspect filters}"
    end
  end

  @doc """
  Receives a test and formats its failure.
  """
  def format_test_failure(test, failures, counter, width, formatter) do
    %ExUnit.Test{name: name, case: case, tags: tags} = test

    test_info(with_counter(counter, "#{name} (#{inspect case})"), formatter)
    <> test_location(with_location(tags), formatter)
    <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
        failure_header(failures, i)
        <> format_kind_reason(kind, reason, width, formatter)
        <> format_stacktrace(stack, case, name, formatter)
       end)
    <> report(tags, failures, width, formatter)
  end

  defp report(tags, failures, width, formatter) do
    case Map.take(tags, List.wrap(tags[:report])) do
      report when map_size(report) == 0 ->
        ""
      report ->
        report_spacing(failures)
        <> extra_info("tags:", formatter)
        <> Enum.map_join(report, "", fn {k, v} ->
            prefix = "       #{k}: "
            prefix <> inspect_multiline(v, byte_size(prefix), width) <> "\n"
           end)
    end
  end

  defp report_spacing([_]), do: ""
  defp report_spacing(_), do: "\n"

  @doc """
  Receives a test case and formats its failure.
  """
  def format_test_case_failure(test_case, failures, counter, width, formatter) do
    %ExUnit.TestCase{name: name} = test_case
    test_case_info(with_counter(counter, "#{inspect name}: "), formatter)
    <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
        failure_header(failures, i)
        <> format_kind_reason(kind, reason, width, formatter)
        <> format_stacktrace(stack, name, nil, formatter)
       end)
  end

  defp format_kind_reason(:error, %ExUnit.AssertionError{} = struct, width, formatter) do
    padding_size = byte_size(@inspect_padding)

    fields = [
      note: if_value(struct.message, &format_banner(&1, formatter)),
      code: if_value(struct.expr, &code_multiline(&1, padding_size)),
      lhs:  if_value(struct.left,  &inspect_multiline(&1, padding_size, width)),
      rhs:  if_value(struct.right, &inspect_multiline(&1, padding_size, width))
    ]
    if formatter.(:colors_enabled?, nil) do
      fields ++ [diff: format_diff(struct, formatter)]
    else
      fields
    end
    |> filter_interesting_fields()
    |> format_each_field(formatter)
    |> make_into_lines(@counter_padding)
  end

  defp format_kind_reason(kind, reason, _width, formatter) do
    error_info Exception.format_banner(kind, reason), formatter
  end

  defp filter_interesting_fields(fields) do
    Enum.filter(fields, fn {_, value} ->
      value != ExUnit.AssertionError.no_value
    end)
  end

  defp format_each_field(fields, formatter) do
    Enum.map(fields, fn {label, value} ->
      format_label(label, formatter) <> value
    end)
  end

  defp if_value(value, fun) do
    if value == ExUnit.AssertionError.no_value do
      value
    else
      fun.(value)
    end
  end

  defp format_label(:note, _formatter), do: ""

  defp format_label(label, formatter) do
    formatter.(:error_info, String.ljust("#{label}:", byte_size(@label_padding)))
  end

  defp format_banner(value, formatter) do
    value = String.replace(value, "\n", "\n" <> @counter_padding)
    formatter.(:error_info, value)
  end

  defp code_multiline(expr, padding_size) when is_binary(expr) do
    padding = String.duplicate(" ", padding_size)
    String.replace(expr, "\n", "\n" <> padding)
  end

  defp code_multiline(expr, padding_size) do
    code_multiline(Macro.to_string(expr), padding_size)
  end

  defp inspect_multiline(expr, padding_size, width) do
    padding = String.duplicate(" ", padding_size)
    width = if width == :infinity, do: width, else: width - padding_size
    inspect(expr, [pretty: true, width: width])
    |> String.replace("\n", "\n" <> padding)
  end

  defp make_into_lines(reasons, padding) do
    padding <> Enum.join(reasons, "\n" <> padding) <> "\n"
  end

  defp format_diff(struct, formatter) do
    if_value(struct.left, fn left ->
      if_value(struct.right, fn right ->
        format_diff(left, right, formatter) || ExUnit.AssertionError.no_value
      end)
    end)
  end

  defp format_diff(<<left::bytes>>, <<right::bytes>>, formatter) do
    String.myers_difference(left, right)
    |> Enum.map_join(&format_diff_fragment(&1, formatter))
    |> String.replace("\n", "\n" <> @inspect_padding)
  end

  defp format_diff(%{} = left, %{} = right, formatter) do
    {surplus, altered} =
      Enum.reduce(left, {[], []}, fn({key, val1}, {surplus, altered} = acc) ->
        case Map.fetch(right, key) do
          {:ok, ^val1} ->
            acc
          {:ok, val2} ->
            {surplus, [{key, {val1, val2}} | altered]}
          :error ->
            {[{key, val1} | surplus], altered}
        end
      end)
    missing = Enum.reject(right, fn {key, _} -> Map.has_key?(left, key) end)
    result = Enum.reduce(missing, [], fn({key, val}, acc) ->
      [formatter.(:diff_insert, inspect(key) <> " => " <> inspect(val)) | acc]
    end)
    result = Enum.reduce(surplus, result, fn({key, val}, acc) ->
      [formatter.(:diff_delete, inspect(key) <> " => " <> inspect(val)) | acc]
    end)
    result = Enum.reduce(altered, result, fn({key, {val1, val2}}, acc) ->
      [inspect(key) <> " => " <> format_inner_diff(val1, val2, formatter) | acc]
    end)
    result = Enum.intersperse(result, ", ") |> IO.iodata_to_binary
    "%{" <> result <> "}"
  end

  defp format_diff(left, right, formatter)
  when is_number(left) and is_number(right)
  when is_tuple(left) and is_tuple(right)
  when is_list(left) and is_list(right) do
    format_diff(inspect(left), inspect(right), formatter)
  end

  defp format_diff(_left, _right, _formatter), do: nil

  defp format_inner_diff(<<left::bytes>>, <<right::bytes>>, formatter) do
    format_diff(inspect(left), inspect(right), formatter)
  end

  defp format_inner_diff(left, right, formatter) do
    if result = format_diff(left, right, formatter) do
      result
    else
      formatter.(:diff_delete, inspect(left)) <>
      formatter.(:diff_insert, inspect(right))
    end
  end

  defp format_diff_fragment({:eq, <<ch1::utf8, ch2::utf8, ch3::utf8>> <> rest}, _) do
    slice = String.slice(rest, -2, 2)
    fill = if slice != rest, do: "...", else: <<ch3::utf8>>
    <<ch1::utf8, ch2::utf8>> <> fill <> slice
  end

  defp format_diff_fragment({:eq, content}, _), do: content

  defp format_diff_fragment({:del, content}, formatter) do
    formatter.(:diff_delete, content)
  end

  defp format_diff_fragment({:ins, content}, formatter) do
    formatter.(:diff_insert, content)
  end

  defp format_stacktrace([], _case, _test, _color) do
    ""
  end

  defp format_stacktrace(stacktrace, test_case, test, color) do
    extra_info("stacktrace:", color) <>
      Enum.map_join(stacktrace,
        fn(s) -> stacktrace_info format_stacktrace_entry(s, test_case, test), color end)
  end

  defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
    "#{location[:file]}:#{location[:line]}"
  end

  defp format_stacktrace_entry(s, _test_case, _test) do
    format_stacktrace_entry(s)
  end

  defp with_location(tags) do
    "#{Path.relative_to_cwd(tags[:file])}:#{tags[:line]}"
  end

  defp failure_header([_], _), do: ""
  defp failure_header(_, i), do: "\n#{@counter_padding}Failure ##{i+1}\n"

  defp with_counter(counter, msg) when counter < 10  do "  #{counter}) #{msg}" end
  defp with_counter(counter, msg) when counter < 100 do  " #{counter}) #{msg}" end
  defp with_counter(counter, msg)                    do   "#{counter}) #{msg}" end

  defp test_case_info(msg, nil),       do: msg <> "failure on setup_all callback, tests invalidated\n"
  defp test_case_info(msg, formatter), do: test_case_info(formatter.(:test_case_info, msg), nil)

  defp test_info(msg, nil),       do: msg <> "\n"
  defp test_info(msg, formatter), do: test_info(formatter.(:test_info, msg), nil)

  defp test_location(msg, nil),       do: "     " <> msg <> "\n"
  defp test_location(msg, formatter), do: test_location(formatter.(:location_info, msg), nil)

  defp error_info(msg, nil) do
    "     " <> String.replace(msg, "\n", "\n     ") <> "\n"
  end

  defp error_info(msg, formatter), do: error_info(formatter.(:error_info, msg), nil)

  defp extra_info(msg, nil),       do: "     " <> msg <> "\n"
  defp extra_info(msg, formatter), do: extra_info(formatter.(:extra_info, msg), nil)

  defp stacktrace_info(msg, nil),       do: "       " <> msg <> "\n"
  defp stacktrace_info(msg, formatter), do: stacktrace_info(formatter.(:stacktrace_info, msg), nil)
end

defmodule ExUnit.Formatter do
  @moduledoc """
  This module holds helper functions related to formatting and contains
  documentation about the formatting protocol.

  Formatters are registered at the `ExUnit.EventManager` event manager and
  will be send events by the runner.

  The following events are possible:

    * `{:suite_started, opts}` -
      the suite has started with the specified options to the runner.

    * `{:suite_finished, run_us, load_us}` -
      the suite has finished. `run_us` and `load_us` are the run and load
      times in microseconds respectively.

    * `{:case_started, test_case}` -
      a test case has started. See `ExUnit.TestCase` for details.

    * `{:case_finished, test_case}` -
      a test case has finished. See `ExUnit.TestCase` for details.

    * `{:test_started, test_case}` -
      a test case has started. See `ExUnit.Test` for details.

    * `{:test_finished, test_case}` -
      a test case has finished. See `ExUnit.Test` for details.

  """

  @type id :: term
  @type test_case :: ExUnit.TestCase.t
  @type test :: ExUnit.Test.t
  @type run_us :: pos_integer
  @type load_us :: pos_integer | nil

  import Exception, only: [format_stacktrace_entry: 1]

  @label_padding   "      "
  @counter_padding "     "
  @inspect_padding @counter_padding <> @label_padding

  @doc """
  Formats time taken running the test suite.

  It receives the time spent running the tests and
  optionally the time spent loading the test suite.

  ## Examples

      iex> format_time(10000, nil)
      "Finished in 0.01 seconds"

      iex> format_time(10000, 20000)
      "Finished in 0.03 seconds (0.02s on load, 0.01s on tests)"

      iex> format_time(10000, 200000)
      "Finished in 0.2 seconds (0.2s on load, 0.01s on tests)"

  """
  @spec format_time(run_us, load_us) :: String.t
  def format_time(run_us, nil) do
    "Finished in #{run_us |> normalize_us |> format_us} seconds"
  end

  def format_time(run_us, load_us) do
    run_us  = run_us |> normalize_us
    load_us = load_us |> normalize_us

    total_us = run_us + load_us
    "Finished in #{format_us total_us} seconds (#{format_us load_us}s on load, #{format_us run_us}s on tests)"
  end

  defp normalize_us(us) do
    div(us, 10000)
  end

  defp format_us(us) do
    if us < 10 do
      "0.0#{us}"
    else
      us = div us, 10
      "#{div(us, 10)}.#{rem(us, 10)}"
    end
  end

  @doc """
  Formats filters used to constrain cases to be run.

  ## Examples

      iex> format_filters([run: true, slow: false], :include)
      "Including tags: [run: true, slow: false]"

  """
  @spec format_filters(Keyword.t, atom) :: String.t
  def format_filters(filters, type) do
    case type do
      :include -> "Including tags: #{inspect filters}"
      :exclude -> "Excluding tags: #{inspect filters}"
    end
  end

  @doc """
  Receives a test and formats its failure.
  """
  def format_test_failure(test, failures, counter, width, formatter) do
    %ExUnit.Test{name: name, case: case, tags: tags} = test

    test_info(with_counter(counter, "#{name} (#{inspect case})"), formatter)
    <> test_location(with_location(tags), formatter)
    <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
        failure_header(failures, i)
        <> format_kind_reason(kind, reason, width, formatter)
        <> format_stacktrace(stack, case, name, formatter)
       end)
    <> report(tags, failures, width, formatter)
  end

  defp report(tags, failures, width, formatter) do
    case Map.take(tags, List.wrap(tags[:report])) do
      report when map_size(report) == 0 ->
        ""
      report ->
        report_spacing(failures)
        <> extra_info("tags:", formatter)
        <> Enum.map_join(report, "", fn {k, v} ->
            prefix = "       #{k}: "
            prefix <> inspect_multiline(v, byte_size(prefix), width) <> "\n"
           end)
    end
  end

  defp report_spacing([_]), do: ""
  defp report_spacing(_), do: "\n"

  @doc """
  Receives a test case and formats its failure.
  """
  def format_test_case_failure(test_case, failures, counter, width, formatter) do
    %ExUnit.TestCase{name: name} = test_case
    test_case_info(with_counter(counter, "#{inspect name}: "), formatter)
    <> Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, i} ->
        failure_header(failures, i)
        <> format_kind_reason(kind, reason, width, formatter)
        <> format_stacktrace(stack, name, nil, formatter)
       end)
  end

  defp format_kind_reason(:error, %ExUnit.AssertionError{} = struct, width, formatter) do
    padding_size = byte_size(@inspect_padding)

    fields = [
      note: if_value(struct.message, &format_banner(&1, formatter)),
      code: if_value(struct.expr, &code_multiline(&1, padding_size)),
      lhs:  if_value(struct.left,  &inspect_multiline(&1, padding_size, width)),
      rhs:  if_value(struct.right, &inspect_multiline(&1, padding_size, width))
    ]
    if formatter.(:colors_enabled?, nil) do
      fields ++ [diff: format_diff(struct, formatter)]
    else
      fields
    end
    |> filter_interesting_fields()
    |> format_each_field(formatter)
    |> make_into_lines(@counter_padding)
  end

  defp format_kind_reason(kind, reason, _width, formatter) do
    error_info Exception.format_banner(kind, reason), formatter
  end

  defp filter_interesting_fields(fields) do
    Enum.filter(fields, fn {_, value} ->
      value != ExUnit.AssertionError.no_value
    end)
  end

  defp format_each_field(fields, formatter) do
    Enum.map(fields, fn {label, value} ->
      format_label(label, formatter) <> value
    end)
  end

  defp if_value(value, fun) do
    if value == ExUnit.AssertionError.no_value do
      value
    else
      fun.(value)
    end
  end

  defp format_label(:note, _formatter), do: ""

  defp format_label(label, formatter) do
    formatter.(:error_info, String.ljust("#{label}:", byte_size(@label_padding)))
  end

  defp format_banner(value, formatter) do
    value = String.replace(value, "\n", "\n" <> @counter_padding)
    formatter.(:error_info, value)
  end

  defp code_multiline(expr, padding_size) when is_binary(expr) do
    padding = String.duplicate(" ", padding_size)
    String.replace(expr, "\n", "\n" <> padding)
  end

  defp code_multiline(expr, padding_size) do
    code_multiline(Macro.to_string(expr), padding_size)
  end

  defp inspect_multiline(expr, padding_size, width) do
    padding = String.duplicate(" ", padding_size)
    width = if width == :infinity, do: width, else: width - padding_size
    inspect(expr, [pretty: true, width: width])
    |> String.replace("\n", "\n" <> padding)
  end

  defp make_into_lines(reasons, padding) do
    padding <> Enum.join(reasons, "\n" <> padding) <> "\n"
  end

  defp format_diff(struct, formatter) do
    if_value(struct.left, fn left ->
      if_value(struct.right, fn right ->
        if same_data_type?(left, right) do
          format_diff(left, right, formatter)
        else
          ExUnit.AssertionError.no_value
        end
      end)
    end)
  end

  defp format_diff(left, right, formatter) when is_binary(left) and is_binary(right) do
    String.myers_difference(left, right)
    |> Enum.map_join(&format_diff_fragment(&1, formatter))
    |> String.replace("\n", "\n" <> @inspect_padding)
  end

  defp format_diff(left, right, formatter) when is_map(left) and is_map(right) do
    {surplus, altered} =
      Enum.reduce(left, {[], []}, fn({key, val1}, {surplus, altered} = acc) ->
        case Map.fetch(right, key) do
          {:ok, ^val1} ->
            acc
          {:ok, val2} ->
            {surplus, [{key, {val1, val2}} | altered]}
          :error ->
            {[{key, val1} | surplus], altered}
        end
      end)
    missing = Enum.reject(right, fn {key, _} -> Map.has_key?(left, key) end)
    result = Enum.reduce(missing, [], fn({key, val}, acc) ->
      [formatter.(:diff_insert, inspect(key) <> " => " <> inspect(val)) | acc]
    end)
    result = Enum.reduce(surplus, result, fn({key, val}, acc) ->
      [formatter.(:diff_delete, inspect(key) <> " => " <> inspect(val)) | acc]
    end)
    result = Enum.reduce(altered, result, fn({key, {val1, val2}}, acc) ->
      delete = formatter.(:diff_delete, inspect(val1))
      insert = formatter.(:diff_insert, inspect(val2))
      [inspect(key) <> " => " <> delete <> insert | acc]
    end)
    result = Enum.intersperse(result, ", ") |> IO.iodata_to_binary
    "%{" <> result <> "}"
  end

  defp format_diff(left, right, formatter) do
    format_diff(inspect(left), inspect(right), formatter)
  end

  defp format_diff_fragment({:eq, <<ch1::utf8, ch2::utf8, ch3::utf8>> <> rest}, _) do
    slice = String.slice(rest, -2, 2)
    fill = if slice != rest, do: "...", else: <<ch3::utf8>>
    <<ch1::utf8, ch2::utf8>> <> fill <> slice
  end

  defp format_diff_fragment({:eq, content}, _), do: content

  defp format_diff_fragment({:del, content}, formatter) do
    formatter.(:diff_delete, content)
  end

  defp format_diff_fragment({:ins, content}, formatter) do
    formatter.(:diff_insert, content)
  end

  defp same_data_type?(left, right)
  when is_number(left) and is_number(right)
  when is_binary(left) and is_binary(right)
  when is_tuple(left) and is_tuple(right)
  when is_list(left) and is_list(right)
  when is_map(left) and is_map(right), do: true

  defp same_data_type?(_left, _right), do: false

  defp format_stacktrace([], _case, _test, _color) do
    ""
  end

  defp format_stacktrace(stacktrace, test_case, test, color) do
    extra_info("stacktrace:", color) <>
      Enum.map_join(stacktrace,
        fn(s) -> stacktrace_info format_stacktrace_entry(s, test_case, test), color end)
  end

  defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
    "#{location[:file]}:#{location[:line]}"
  end

  defp format_stacktrace_entry(s, _test_case, _test) do
    format_stacktrace_entry(s)
  end

  defp with_location(tags) do
    "#{Path.relative_to_cwd(tags[:file])}:#{tags[:line]}"
  end

  defp failure_header([_], _), do: ""
  defp failure_header(_, i), do: "\n#{@counter_padding}Failure ##{i+1}\n"

  defp with_counter(counter, msg) when counter < 10  do "  #{counter}) #{msg}" end
  defp with_counter(counter, msg) when counter < 100 do  " #{counter}) #{msg}" end
  defp with_counter(counter, msg)                    do   "#{counter}) #{msg}" end

  defp test_case_info(msg, nil),       do: msg <> "failure on setup_all callback, tests invalidated\n"
  defp test_case_info(msg, formatter), do: test_case_info(formatter.(:test_case_info, msg), nil)

  defp test_info(msg, nil),       do: msg <> "\n"
  defp test_info(msg, formatter), do: test_info(formatter.(:test_info, msg), nil)

  defp test_location(msg, nil),       do: "     " <> msg <> "\n"
  defp test_location(msg, formatter), do: test_location(formatter.(:location_info, msg), nil)

  defp error_info(msg, nil) do
    "     " <> String.replace(msg, "\n", "\n     ") <> "\n"
  end

  defp error_info(msg, formatter), do: error_info(formatter.(:error_info, msg), nil)

  defp extra_info(msg, nil),       do: "     " <> msg <> "\n"
  defp extra_info(msg, formatter), do: extra_info(formatter.(:extra_info, msg), nil)

  defp stacktrace_info(msg, nil),       do: "       " <> msg <> "\n"
  defp stacktrace_info(msg, formatter), do: stacktrace_info(formatter.(:stacktrace_info, msg), nil)
end