require('spec/setup/busted')()

local Position = require('__stdlib__/stdlib/area/position')
local P = Position

describe('Position', function ()

    local C = spy.on(Position, 'construct')
    local N = spy.on(Position, 'new')
    local L = spy.on(Position, 'load')
    local K = spy.on(Position, 'from_key')
    local S = spy.on(Position, 'from_string')
    local a, a1
    local zero
    local ps, sq, co

    before_each(function()
        a = {x = 1, y = 1}
        a1 = {1, 1}
        zero = {x = 0, y = 0}
        ps = {
            a = Position(4, 8),
            b = Position(-12, 13),
            c = Position(8, -6),
            d = Position(-10, -11)
        }
        sq = {
            a = Position(12, 12),
            b = Position(-12, 12),
            c = Position(12, -12),
            d = Position(-12, -12)
        }
        co = {
            a = Position(-12, -12),
            b = Position(12, 12),
            c = Position(-12, 12),
            d = Position(12, -12),
        }
        C:clear()
        N:clear()
        L:clear()
        K:clear()
        S:clear()
    end)

    describe('Constructors', function ()

        describe('.__index', function()
            it('should point to core', function()
                assert.is.truthy(getmetatable(Position)._VERSION)
            end)
        end)

        describe('.new', function()
            it('should create a new version from table', function()
                assert.not_same(a1, Position.new(a1))
                assert.same(a1[1], Position.new(a1).x)
            end)

            it('should have the correct metatable', function()
                assert.same('position', getmetatable(Position.new(a1)).__class)
            end)
        end)

        describe('.construct', function()
            it('should construct from parameters', function()
                assert.same({x = -4, y = 21}, Position.construct(-4, 21))
                assert.same(3, Position.construct(3).y) -- x copied to y
                assert.same(3, Position.construct(1, 3).y) -- x and y
                assert.same(0, Position.construct().y) -- empty params
                assert.same(0, Position.construct(nil, 4).x)
                assert.spy(C).was_called(5)
            end)
            it('should construct from params if self is passed as first argument', function()
                assert.same(3, Position.construct(Position.new(a1), 1, 3).y)
                assert.same(3, Position(1, 3).y)
            end)
        end)

        describe('.__call', function()
            it('should call the correct constructor', function()
                local str = spy.on(Position, 'from_string')
                Position(a1)
                Position(1)
                Position('1, 2')
                Position('{1, 2}')
                assert.spy(C).was_called(2)
                assert.spy(N).was_called(2)
                assert.spy(str).was_called(2)
            end)

            it('should create correctly', function()
                assert.same({x = 0, y = 0}, Position())
                assert.same({x = 1, y = 1}, Position(1, 1))
                assert.same({x = 1, y = 1}, Position(1))
                assert.same({x = 1, y = 1}, Position(a1))
            end)
        end)

        describe('.from_string', function()
            it('should construct from a string', function()
                assert.same(a, Position.from_string('{x = 1, y = 1}'))
                assert.spy(L).was_called(1)
                assert.same(a, Position.from_string('{1, 1}'))
                assert.spy(N).was_called(1)
                assert.same({x = 1, y = 2}, Position.from_string('1, 2'))
                assert.spy(C).was_called(1)
            end)
        end)

        describe('.from_key', function()
            it('should create correctly', function()
                assert.same({x = 1, y = 2}, Position.from_key('1,2'))
                assert.same({x = 12, y = -3}, Position.from_key('12,-3'))
                assert.same({x = -12, y = 3}, Position.from_key('-12,3'))
                assert.same({x = -12, y = -3}, Position.from_key('-12,-3'))
            end)
        end)

        describe('.load', function()
            it('should set the metatable to a valid table', function()
                assert.same('position', getmetatable(Position.load({x = 3, y = -2})).__class)
                assert.has_no_error(function() Position.load({x = 1, y = -2}) end)
                assert.spy(L).was.called(2)
            end)
        end)
    end)

    describe('Methods', function()


        before_each(function ()

        end)

        --[[
            add, subtract, multiply, divide, mod, unary, abs, center, ceil, floor, center, between,
            perpendicular, swap, offset_along_line, translate, trim, projection, reflection,
            average, min, mix, closest, farthest, mix_xy, max_xy
        ]]
        it('.add', function ()
            assert.same(P(2, 2), P(1, 1):add(1))
            assert.same(P(2, 3), P(1, 1):add(1, 2))
            assert.same(P(3, -1), P(1, 1):add{2, -2})
            assert.same(P(0, 0), P(1, 1):add(-1))
        end)
        it('.subtract', function ()
            assert.same(P(2, 2), P(3, 3):subtract(1))
            assert.same(P(3, 3), P(5, 1):subtract{2, -2})
            assert.same(P(0, 0), P(-1, -1):subtract(-1))
        end)
        it('.divide', function ()
            assert.same(P(2, 4), P(4, 8):divide(2))
            assert.same(P(2, 2), P(4, 8):divide{2, 4})
            assert.same(P(2, -2), P(4, 8):divide{2, -4})
        end)
        it('.multiply', function ()
            assert.same(P(2, 2), P(4, 4):multiply(.5))
            assert.same(P(2, -2), P(4, 4):multiply{.5, -.5})
        end)
        it('.mod', function ()
            assert.same(P(0, 0), P(4, 4):mod(2))
            assert.same(P(0, 0), P(4, 4):mod{2, -2})
        end)
        it('.closest', function ()
            assert.same(ps.a, P():closest({ps.b, ps.c, ps.d, ps.a}))
            assert.same(ps.a, P.closest(P(zero)(), {ps.b, ps.c, ps.d, ps.a}))
        end)
        it('.farthest', function ()
            assert.same(ps.b, P():farthest({ps.b, ps.c, ps.d, ps.a}))
            assert.same(ps.b, P.farthest(P(zero)(), {ps.b, ps.c, ps.d, ps.a}))
        end)
        it('.flip', function ()
            assert.same(P(2, 2), P(-2, -2):flip())
        end)
        it('.normalize', function ()
            assert.same({x = 2.46, y = 4.72}, P(2.4563787, 4.723444432):normalize())
        end)
        it('.abs', function ()
            assert.same(P(2, 2), P(-2, 2):abs())
        end)
        it('.ceil', function ()
            assert.same(P(2, 3), P(2.4, 2.8):round())
            assert.same(P(-2, -3), P(-2.4, -2.8):round())
        end)
        it('.ceil', function ()
            assert.same(P(2, -2), P(1.24, -2.34):ceil())
        end)
        it('.floor', function ()
            assert.same(P(1, -3), P(1.24, -2.34):floor())
        end)
        it('.center', function ()
            assert.same(P(1.5, -1.5), P(1.23, -1.65):center())
        end)
        it('.between', function ()
            assert.same(P(-6, -6), P(0, 0):between(P(-12, -12)))
            assert.same(P(6, -6), P(0, 0):between(P(12, -12)))
            assert.same(P(6, 6), P(0, 0):between(P(12, 12)))
            assert.same(P(-6, 6), P(0, 0):between(P(-12, 12)))
        end)
        it('.perpendicular', function ()
            assert.same(P(-12, 12), P(12, 12):perpendicular())
        end)
        it('.swap', function ()
            assert.same(P(1.25, -2.55), P(-2.55, 1.25):swap())
        end)
        it('.trim', function ()
            local max = P(10, 10)
            assert.same(P(3.5355339059327378, 3.5355339059327378), max:trim(5))
            max = P(10, 0)
            assert.same(P(5, 0), max:trim(5))
        end)
        it('.lerp', function ()
            local from, to = P(), P(-10, 0)
            assert.same(P(-8, 0), from:lerp(to, .8))
        end)
        it('.offset_along_line', function ()
            assert.same(P(4.59, 4.59), P():offset_along_line(P(6,6), 2))
        end)
        it('.translate', function ()
            local pos = P{1, -4}:store()
            assert.same({x = 1, y = -5},  pos:translate(defines.direction.north, 1):store())
            assert.same({x = 1, y = -3},  pos:recall():translate(defines.direction.south, 2):store())
            assert.same({x = 2, y = -3},  pos:recall():translate(defines.direction.east, 1):store())
            assert.same({x = -1, y = -3}, pos:recall():translate(defines.direction.west, 3):store())
            assert.same({x = 0, y = -2},  pos:recall():translate(defines.direction.southeast, 1):store())
            assert.same({x = -1, y = -1}, pos:recall():translate(defines.direction.southwest, 1):store())
            assert.same({x = 0, y = -2},  pos:recall():translate(defines.direction.northeast, 1):store())
            assert.same({x = -1, y = -3}, pos:recall():translate(defines.direction.northwest, 1):store())
            assert.same({x = -1, y = 0},  pos:recall():translate(defines.direction.north, -3):store())
        end)
        it('projection', function ()
            local b, c = P(5, 10), P(10, 10)
            assert.same(P(7.5, 7.5), b:projection(c))
        end)
        it('reflection', function ()
            local b, c = P(5, 10), P(10, 10)
            assert.same(P(10, 5), b:reflection(c))
        end)
    end)

    describe('New Position Methods', function ()
        it('.average', function ()
            assert.same(zero, P.average({sq.a, sq.b, sq.c, sq.d}))
            assert.same(zero, sq.a:average({sq.b, sq.c, sq.d}))
        end)
        it('.min', function ()
            local m = P(4, 8)
            assert.same(m, P.min({ps.a, ps.b, ps.c, ps.d}))
            assert.same(m, ps.a:min({ps.b, ps.c, ps.d}))
        end)
        it('.max', function ()
            local m = P(-12, 13)
            assert.same(m, P.max({ps.a, ps.b, ps.c, ps.d}))
            assert.same(m, ps.a:max({ps.b, ps.c, ps.d}))
        end)
        it('.min_xy', function ()
            local m = P(-12, -11)
            assert.same(m, P.min_xy({ps.a, ps.b, ps.c, ps.d}))
            assert.same(m, ps.a:min_xy({ps.b, ps.c, ps.d}))
        end)
        it('.max_xy', function ()
            local m = P(8, 13)
            assert.same(m, P.max_xy({ps.a, ps.b, ps.c, ps.d}))
            assert.same(m, ps.a:max_xy({ps.b, ps.c, ps.d}))
        end)
        it('.intersection', function ()
            assert.same(P(), P.intersection(co.a, co.b, co.c, co.d))
            assert.not_same(P(), P.intersection(co.a, co.c, co.b, co.d))
        end)
    end)

    describe('Position Conversion Functions', function ()
        --[[
            from_pixels, to_pixels, to_chunk_position, from_chunk_position,
        ]]
        it('.from_pixels', function ()
            assert.same(P(2), P(64):from_pixels())
        end)
        it('.to_pixels', function ()
            assert.same(P(64), P(2):to_pixels())
        end)
        it('.to_chunk_position', function ()
            assert.same(P(0, -1), P(16.5, -16.42):to_chunk_position())
        end)
        it('.from_chunk_position', function ()
            assert.same(P(0, -32), P(0, -1):from_chunk_position())
        end)
    end)

    describe('Functions', function()
        --[[
            increment, atan2, angle, cross, dot, inside, len, len_squared, to_string, to_key, unpack, pack, equals,
            less_than, less_than_eq, distance_squared, distance, manhattan_distance,
            is_position, is_zero, is_set, direction_to, simple_direction_to
        ]]
        describe('.increment', function()
            local pos = Position()

            it('should error with no position argument', function()
                assert.has_error(function() return Position.incremement() end)
            end)

            it('should return a function closure', function()
                local f = Position.increment(pos)
                assert.is_true(type(f)=='function')
            end)

            it('should not increment on the first call by default', function()
                local f = Position.increment(pos, 1)
                assert.same(Position(), f())
            end)

            it('should increment the first call when requested', function()
                local f = Position.increment(pos, 1, nil, true)
                assert.same({x=1, y=0}, f())
                assert.same({x=2, y=0}, f())
            end)

            it('should return the same position', function()
                local f = Position.increment(pos)
                assert.same({x=0, y=0}, f())
                assert.same({x=0, y=0}, f())
                local g = Position():increment(nil, nil, true)
                assert.same({x=0, y=0}, g())
                assert.same({x=0, y=0}, g())
            end)

            it('should increment using the defaults', function()
                local f = Position.increment(pos, 0, -1)
                assert.same({x=0, y=0}, f())
                assert.same({x=0, y=-1}, f())
            end)

            it('should increment using passed values', function()
                local f = Position.increment(pos)
                assert.same({x=0, y=0}, f(0, -1))
                assert.same({x=0, y=-1}, f(0, -1))
                assert.same({x=0, y=-3}, f(0, -2))
            end)

            it('should increment using passed values with defaults set', function()
                local f = Position.increment(pos, -1, -1)
                assert.same({x=0, y=0}, f())
                assert.same({x=-1, y=-1}, f())
                assert.same({x=0, y=1}, f(1, 2))
                assert.same({x=1, y=4}, f(1, 3))
            end)
        end)
        it('.atan2', function ()
            assert.same(-1.5707963267948966, P(10, 0):atan2(P(5,0)))
        end)
        it('.angle', function ()
            assert.same(90, P(10, 0):angle(P(0, 10)))
        end)
        it('.cross', function ()
            assert.same(50, P(10, 0):cross(P(5, 5)))
        end)
        it('.dot', function ()
            assert.same(50, P(10, 0):dot(P(5, 5)))
        end)
        it('.len', function ()
            assert.same(10, P(10, 0):len())
        end)
        it('.len_squared', function ()
            assert.same(100, P(10, 0):len_squared())
        end)
        it('.to_string', function ()
            local pos = P{1, -4}
            assert.same('{x = 1, y = -4}', Position.to_string(pos))
            assert.has_error(function() Position.to_string() end)
        end)
        it('.to_key', function ()
            assert.same('3,-5', P(3, -5):to_key())
            assert.same('2,0', P('2,0'):to_key())
            assert.spy(S).was_called(1)
        end)
        it('.unpack', function ()
            local x, y = Position(1, 2):unpack()
            assert.same(x, 1)
            assert.same(y, 2)
        end)
        it('.pack', function ()
            assert.same({3, 4}, P(3, 4):pack())
        end)
        describe('.equals', function()
            it('compares shallow equality in positions', function()
                local pos1 = P{1, -4}
                local pos2 = pos1

                assert.is_true(Position.equals(pos1, pos2))
                assert.is_false(Position.equals(pos1, nil))
                assert.is_false(Position.equals(nil, pos2))
                assert.is_false(Position.equals(nil, nil))
            end)

            it('compares positions', function()
                local pos1 = P{1, -4}
                local pos2 = P{ x = 3, y = -2}
                local pos3 = P{-1, -4}
                local pos4 = P{ x = 1, y = -4}

                assert.is_true(Position.equals(pos1, pos1))
                assert.is_false(Position.equals(pos1, pos2))
                assert.is_false(Position.equals(pos1, pos3))
                assert.is_true(Position.equals(pos1, pos4))
            end)
        end)
        it('.distance_squared', function ()
            local pos_a = P{1, -4}
            local pos_b = P{3, -2}
            assert.same(8, Position.distance_squared(pos_a, pos_b))
            assert.same(8, Position.distance_squared(pos_b, pos_a))
        end)
        it('.distance', function ()
            local pos_a = P{5, -5}
            local pos_b = P{10, 0}
            assert.same(math.sqrt(50), Position.distance(pos_a, pos_b))
            assert.same(math.sqrt(50), Position.distance(pos_b, pos_a))
        end)
        it('.manhattan_distance', function ()
            local pos_a = P{5, -5}
            local pos_b = P{10, 0}
            assert.same(10, Position.manhattan_distance(pos_a, pos_b))
            assert.same(10, Position.manhattan_distance(pos_b, pos_a))

            pos_a = P{1, -4}
            pos_b = P{3, -2}
            assert.same(4, Position.manhattan_distance(pos_a, pos_b))
            assert.same(4, Position.manhattan_distance(pos_b, pos_a))
        end)
        it('.is_position', function ()
            local p = P()
            assert.is_true(P.is_position(zero))
            assert.is_true(p:is_position())
            assert.is_true(P.is_position(a1))
        end)
        it('.is_zero', function ()
            assert.is_true(Position():is_zero())
            assert.is_not_true(Position(1, 0):is_zero())
            assert.is_true(Position.is_zero({x = 0, y = 0}))
        end)
        it('.is_Position', function ()
            local p = P()
            assert.is_true(p.is_Position(p))
            assert.is_true(P.is_Position(p))
            assert.is_true(p:is_Position())
            assert.is_false(P.is_Position(zero))
        end)
        it('.direction_to', function ()
            local mid = P()
            local b, c, d, e = P(0, -1), P(1, 0), P(0, 1), P(-1, 0)
            assert.same(0, mid:direction_to(b))
            assert.same(2, mid:direction_to(c))
            assert.same(4, mid:direction_to(d))
            assert.same(6, mid:direction_to(e))
            local f, g, h, i = P(1, -1), P(1, -1), P(-1, 1), P(-1, -1)
            assert.same(6, mid:direction_to(f))
            assert.same(6, mid:direction_to(g))
            assert.same(2, mid:direction_to(h))
            assert.same(6, mid:direction_to(i))
        end)
        it('.less_than', function ()
            local b, c, d = P(5, 10), P(10, 10), P(10, 10)
            assert.is_true(b:less_than(c))
            assert.is_false(c:less_than(d))
        end)
        it('.less_than_eq', function ()
            local b, c, d = P(5, 10), P(10, 10), P(10, 10)
            assert.is_true(b:less_than_eq(c))
            assert.is_true(c:less_than_eq(d))
        end)
        it('.complex_direction_to', function ()
            local mid = P()
            local n, ne, e, se, s, sw, w, nw = P(0, -1), P(1, -1), P(1, 0), P(1, 1), P(0, 1), P(-1, 1), P(-1, 0), P(-1, -1)
            assert.same(0, mid:complex_direction_to(n, true))
            assert.same(1, mid:complex_direction_to(ne, true))
            assert.same(2, mid:complex_direction_to(e, true))
            assert.same(3, mid:complex_direction_to(se, true))
            assert.same(4, mid:complex_direction_to(s, true))
            assert.same(5, mid:complex_direction_to(sw, true))
            assert.same(6, mid:complex_direction_to(w, true))
            assert.same(7, mid:complex_direction_to(nw, true))

            assert.same(2, mid:complex_direction_to(ne, false))
            assert.same(4, mid:complex_direction_to(se, false))
            assert.same(6, mid:complex_direction_to(sw, false))
            assert.same(0, mid:complex_direction_to(nw, false))
        end)
        it('.inside', function ()
            local area = {left_top = {x = -1, y = -1}, right_bottom = {x = 0, y = 0}}
            assert.is_true(P(-0.5, -0.7):inside(area))
            assert.is_false(P(0.5, -0.7):inside(area))
        end)
    end)

    describe('Area Conversion Functions', function()
        --[[
            expand_to_area, to_area, to_chunk_area, to_tile_area
        ]]

        it('.to_tile_area', function()
            local area = {left_top = {x = -1, y = -1}, right_bottom = {x = 0, y = 0}}
            assert.same(area, P(-0.34, -0.75):to_tile_area())
        end)
        it('.to_chunk_area', function()
            local area = {left_top = {x = -32, y = -32}, right_bottom = {x = 0, y = 0}}
            assert.same(area, P(-1, -1):to_chunk_area())
        end)
        it('.expand_to_area', function()
            local pos = { x = 1, y = -4}
            assert.same(pos, Position.expand_to_area(pos, 0).left_top)
            assert.same(pos, Position.expand_to_area(pos, 0).right_bottom)

            local expanded_area = {left_top = { x = -1, y = -6}, right_bottom = { x = 3, y = -2 }}
            assert.same(expanded_area, Position.expand_to_area(pos, 2))
        end)
        it('.to_area', function ()
            local area = {left_top = {x = 1, y = -4}, right_bottom = {x = 6, y = 2}}
            assert.same(area, P(1, -4):to_area(5, 6))
        end)
    end)

    describe('object metamethods', function()
        --[[
            __class, __index. __add, __sub, __mul, __div, __mod, __unm, __len, __eq, __lt, __le,
            __tostring, __concat, __call,
        ]]
        local mta, mtb, mtc
        before_each(function()
            mta = Position() --0, 0
            mtb = Position(1,1)
            mtc = Position(3, 3)
        end)
        it('.__class', function ()
            assert.same('Position', getmetatable(P).__class)
            assert.same('position', getmetatable(mta).__class)
        end)
        it('__index', function()
            assert.same(getmetatable(mta).__index, Position)
        end)
        it('.__call should trigger a constructor()', function()
            local b2 = mta()
            local c2 = b2

            assert.same(mta, b2)
            assert.is_false(rawequal(mta, b2))
            assert.is_true(rawequal(b2, c2))
        end)
        it('.__add', function()
            assert.same(Position(1, 1), mta + mtb)
            assert.same(Position(2, 2), mta + 2)
            assert.same(Position(2, -2), mta + {2, -2})
            assert.same(Position(3, 3), 2 + mtb)
            assert.same(Position(3, -1), {2, -2} + mtb)
        end)
        it('.__sub', function()
            assert.same(Position(-1, -1), mta - mtb)
            assert.same(Position(-2, -2), mta - 2)
            assert.same(Position(-2, 2), mta - {2, -2})
            assert.same(Position(1, 1), 2 - mtb)
            assert.same(Position(1, -3), {2, -2} - mtb)
        end)
        it('.__mul', function()
            assert.same(Position(9, 9), mtc * mtc)
            assert.same(Position(0, 0), mta * 2)
            assert.same(Position(2, -2), mtb * {2, -2})
            assert.same(Position(6, 6), 2 * mtc)
            assert.same(Position(6, -6), {2, -2} * mtc)
        end)
        it('.__div', function()
            assert.same(Position(1, 1), mtc / mtc)
            assert.same(Position(0, 0), mta / 2)
            assert.same(Position(.5, -.5), mtb / {2, -2})
            assert.same(Position(.66666666666666663, .66666666666666663), 2 / mtc)
            assert.same(Position(.66666666666666663, -.66666666666666663), {2, -2} / mtc)
        end)
        it('.__mod', function()
            assert.same(Position(1, 1), mtb % mtc)
            assert.same(Position(0, 0), mtc % mtb)
        end)
        it('.__unm', function()
            assert.same(Position(-1, -1), -mtb)
            assert.same(Position(1, -3), -Position(-1, 3))
        end)
        it('__eq', function()
            local mtd = mta
            local mte = Position()
            local mtf = Position(2,2)

            assert.is_true(mta == mtd)
            assert.is_true(mta == mte)
            assert.not_true(mta == mtf)
            assert.not_true(mta == 0)
        end)
        it('__lt', function()
            local mte = Position(2,2)
            local mtf = Position(-2,-2)

            assert.is_true(mta < mte)
            assert.is_true(mte > mta)
            assert.is_false(mtf < mtb)
        end)
        it('__le', function()
            local mte = Position(2,2)
            local mtf = Position(-2,-2)
            local mtg = Position(2,2)

            assert.is_true(mta <= mte)
            assert.is_true(mte >= mta)
            assert.is_true(mtf <= mte)
            assert.is_true(mta <= mtg)
            assert.is_true(P(-10, -10) <= P(-100, -100))
        end)
        it('__tostring', function()
            local b1 = Position(1,1)
            local b2 = Position(1, 1)
            local c1 = Position(2,2)

            assert.same(tostring(b1), tostring(b2))
            assert.not_same(tostring(mta), tostring(c1))
        end)
        it('__concat', function()
            local q = Position()
            local r = mta()
            assert.same(50, #(q .. ' is concatenated with ' .. r))
        end)
        it('__len ', function()
            assert.same(50, #Position(50, 0))
            assert.same(50, #Position(0, -50))
            assert.same(35.355339059327378, #Position(25, -25))
        end)
    end)
end)
