local tasty = require 'tasty'

local test = tasty.test_case
local group = tasty.test_group
local assert = tasty.assert

return {
  group "Inline" {
    group 'Cite' {
      test('has property `content`', function ()
        local cite = Cite({Emph 'important'}, {})
        assert.are_same(cite.content, {Emph {Str 'important'}})

        cite.content = 'boring'
        assert.are_equal(cite, Cite({Str 'boring'}, {}))
      end),
      test('has list of citations in property `cite`', function ()
        local citations = {
          Citation('einstein1905', 'NormalCitation')
        }
        local cite = Cite('relativity', citations)
        assert.are_same(cite.citations, citations)

        local new_citations = {
          citations[1],
          Citation('Poincaré1905', 'NormalCitation')
        }
        cite.citations = new_citations
        assert.are_equal(cite, Cite({'relativity'}, new_citations))
      end),
    },
    group 'Code' {
      test('has property `attr`', function ()
        local code = Code('true', {id='true', foo='bar'})
        assert.are_equal(code.attr, Attr('true', {}, {{'foo', 'bar'}}))

        code.attr = {id='t', fubar='quux'}
        assert.are_equal(
          Code('true', Attr('t', {}, {{'fubar', 'quux'}})),
          code
        )
      end),
      test('has property `text`', function ()
        local code = Code('true')
        assert.are_equal(code.text, 'true')

        code.text = '1 + 1'
        assert.are_equal(Code('1 + 1'), code)
      end),
    },
    group 'Emph' {
      test('has property `content`', function ()
        local elem = Emph{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Emph{'word'})
      end)
    },
    group 'Image' {
      test('has property `caption` of type Inlines', function ()
        local img = Image('example', 'a.png')
        assert.are_same(img.caption, {Str 'example'})

        img.caption = 'A'
        assert.are_equal(img, Image({'A'}, 'a.png'))
        assert.are_equal(
          Image('example', 'a.png').caption,
          Inlines('example')
        )
      end),
      test('has property `src`', function ()
        local img = Image('example', 'sample.png')
        assert.are_same(img.src, 'sample.png')

        img.src = 'example.svg'
        assert.are_equal(img, Image('example', 'example.svg'))
      end),
      test('has property `title`', function ()
        local img = Image('here', 'img.gif', 'example')
        assert.are_same(img.title, 'example')

        img.title = 'a'
        assert.are_equal(img, Image('here', 'img.gif', 'a'))
      end),
      test('has property `attr`', function ()
        local img = Image('up', 'upwards.png', '', {'up', {'point'}})
        assert.are_same(img.attr, Attr {'up', {'point'}})

        img.attr = Attr {'up', {'point', 'button'}}
        assert.are_equal(
          Image('up', 'upwards.png', nil, {'up', {'point', 'button'}}),
          img
        )
      end)
    },
    group 'Link' {
      test('has property `content`', function ()
        local link = Link('example', 'https://example.org')
        assert.are_same(link.content, {Str 'example'})

        link.content = 'commercial'
        link.target = 'https://example.com'
        assert.are_equal(link, Link('commercial', 'https://example.com'))
      end),
      test('has property `target`', function ()
        local link = Link('example', 'https://example.org')
        assert.are_same(link.content, {Str 'example'})

        link.target = 'https://example.com'
        assert.are_equal(link, Link('example', 'https://example.com'))
      end),
      test('has property `title`', function ()
        local link = Link('here', 'https://example.org', 'example')
        assert.are_same(link.title, 'example')

        link.title = 'a'
        assert.are_equal(link, Link('here', 'https://example.org', 'a'))
      end),
      test('has property `attr`', function ()
        local link = Link('up', '../index.html', '', {'up', {'nav'}})
        assert.are_same(link.attr, Attr {'up', {'nav'}})

        link.attr = Attr {'up', {'nav', 'button'}}
        assert.are_equal(
          Link('up', '../index.html', nil, {'up', {'nav', 'button'}}),
          link
        )
      end)
    },
    group 'Math' {
      test('has property `text`', function ()
        local elem = Math(InlineMath, 'x^2')
        assert.are_same(elem.text, 'x^2')
        elem.text = 'a + b'
        assert.are_equal(elem, Math(InlineMath, 'a + b'))
      end),
      test('has property `mathtype`', function ()
        local elem = Math(InlineMath, 'x^2')
        assert.are_same(elem.mathtype, 'InlineMath')
        elem.mathtype = DisplayMath
        assert.are_equal(elem, Math(DisplayMath, 'x^2'))
      end),
    },
    group 'Note' {
      test('has property `content`', function ()
        local elem = Note{Para {'two', Space(), 'words'}}
        assert.are_same(
          elem.content,
          {Para {Str 'two', Space(), Str 'words'}}
        )
        elem.content = Plain 'word'
        assert.are_equal(elem, Note{'word'})
      end)
    },
    group 'Quoted' {
      test('has property `content`', function ()
        local elem = Quoted('SingleQuote', Emph{'emph'})
        assert.are_same(
          elem.content,
          {Emph{Str 'emph'}}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Quoted(SingleQuote, {'word'}))
      end),
      test('has property `quotetype`', function ()
        local elem = Quoted('SingleQuote', 'a')
        assert.are_same(elem.quotetype, SingleQuote)
        elem.quotetype = 'DoubleQuote'
        assert.are_equal(elem, Quoted(DoubleQuote, {'a'}))
      end)
    },
    group 'SmallCaps' {
      test('has property `content`', function ()
        local elem = SmallCaps{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, SmallCaps{'word'})
      end)
    },
    group 'SoftBreak' {
      test('can be constructed', function ()
        local sb = SoftBreak()
        assert.are_equal(sb.t, 'SoftBreak')
      end)
    },
    group 'Span' {
      test('has property `attr`', function ()
        local elem = Span('one', {'', {'number'}})
        assert.are_same(
          elem.attr,
          Attr('', {'number'})
        )
        elem.attr = {'', {}, {{'a', 'b'}}}
        assert.are_equal(elem, Span({'one'}, {a='b'}))
      end),
      test('has property `content`', function ()
        local elem = Span{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Span{'word'})
      end)
    },
    group 'Str' {
      test('has property `text`', function ()
        local elem = Str 'nein'
        assert.are_same(elem.text, 'nein')
        elem.text = 'doch'
        assert.are_equal(elem, Str 'doch')
      end)
    },
    group 'Strikeout' {
      test('has property `content`', function ()
        local elem = Strikeout{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Strikeout{'word'})
      end)
    },
    group 'Strong' {
      test('has property `content`', function ()
        local elem = Strong{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Strong{'word'})
      end)
    },
    group 'Subscript' {
      test('has property `content`', function ()
        local elem = Subscript{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Subscript{'word'})
      end)
    },
    group 'Superscript' {
      test('has property `content`', function ()
        local elem = Superscript{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Superscript{'word'})
      end)
    },
    group 'Underline' {
      test('has property `content`', function ()
        local elem = Underline{'two', Space(), 'words'}
        assert.are_same(
          elem.content,
          {Str 'two', Space(), Str 'words'}
        )
        elem.content = {'word'}
        assert.are_equal(elem, Underline{'word'})
      end)
    },
  },
  group "Inlines" {
    group 'Constructor' {
      test('splits a string into words', function ()
        assert.are_same(
          Inlines 'Absolute Giganten',
          {Str 'Absolute', Space(), Str 'Giganten'}
        )
      end),
      test('converts single Inline into List', function ()
        assert.are_same(
          Inlines(Emph{Str'Important'}),
          {Emph{Str'Important'}}
        )
      end),
      test('converts elements in a list into Inlines', function ()
        assert.are_same(
          Inlines{'Molecular', Space(), 'Biology'},
          {Str 'Molecular', Space(), Str 'Biology'}
        )
      end),
      test('tabs are treated as space', function ()
        local expected = {
          Str 'Linkin', Space(), Str 'Park', Space(),
          Str '-', Space(), Str 'Papercut'
        }
        assert.are_same(Inlines('Linkin Park\t-\tPapercut'), expected)
      end),
      test('newlines are treated as softbreaks', function ()
        local expected = {
          Str 'Porcupine', Space(), Str 'Tree',
          SoftBreak(), Str '-', SoftBreak(),
          Str 'Blackest',  Space(), Str 'Eyes'
        }
        assert.are_same(
          Inlines('Porcupine Tree\n-\nBlackest Eyes'),
          expected
        )
      end),
      test('can be mapped over', function ()
        local words = Inlines 'good idea'
        assert.are_same(
          words:map(function (x) return x.t end),
          {'Str', 'Space', 'Str'}
        )
      end),
      test('gives sensible error message', function ()
        assert.error_matches(
          function() Inlines(nil) end,
          "Inline, list of Inlines, or string"
        )
      end)
    },
    group 'clone' {
      test('function exists', function ()
        assert.are_equal(type(Inlines({}).clone), 'function')
      end),
      test('clones the list', function ()
        local inlns = Inlines{'Hello,', Space(), 'World!'}
        assert.are_same(inlns, inlns:clone())
      end),
      test('deep-clones the list', function ()
        local inlns = Inlines{Str 'Hello,', Space(), Str 'World!'}
        local copy = inlns:clone()
        copy[1].text = 'Bonjour,'
        assert.are_same(Inlines{Str 'Bonjour,', Space(), Str 'World!'}, copy)
        assert.are_same(Inlines{Str 'Hello,', Space(), Str 'World!'}, inlns)
      end)
    },
    group 'tostring' {
      test('works on an empty list', function ()
        assert.are_equal(
          tostring(Inlines{}),
          '[]'
        )
      end),
      test('simple inlines', function ()
        assert.are_equal(
          tostring(Inlines 'Bonjour, Monsieur !'),
          '[Str "Bonjour,",Space,Str "Monsieur",Space,Str "!"]'
        )
      end),
    },
    group 'walk' {
      test('modifies Inline subelements', function ()
        assert.are_same(
          Inlines 'Hello, Jake!',
          (Inlines 'Hello, World!'):walk{
            Str = function (str)
              return str.text == 'World!' and Str('Jake!') or nil
            end
          }
        )
      end),
    }
  },
  group 'walk' {
    test('modifies Inline subelements', function ()
      local span = Span 'Hello, World!'
      local expected = Span 'Hello, John!'
      assert.are_equal(
        expected,
        span:walk{
          Str = function (str)
            return str.text == 'World!' and Str('John!') or nil
          end
        }
      )
    end),
    test('applies filter only on subtree', function ()
      local str = Str 'Hello'
      assert.are_equal(
        Str 'Hello',
        str:walk{
          Str = function (str)
            return str.text == 'Hello' and Str('Goodbye') or nil
          end
        }
      )
    end),
    test('modifies blocks in notes', function ()
      local note = Note{Para 'The proof is trivial.'}
      assert.are_equal(
        Note{Plain 'The proof is trivial.'},
        note:walk{
          Para = function (para)
            return Plain(para.content)
          end
        }
      )
    end),
    test('uses `Inlines` for lists of inlines', function ()
      local span = Span{Emph 'Kid A'}
      assert.are_equal(
        Span{Emph 'Kid A+'},
        span:walk{
          Inlines = function (inlns)
            if Span(inlns) == Span 'Kid A' then
              return Inlines 'Kid A+'
            end
          end
        }
      )
    end),
    test('handles inline elements before inline lists', function ()
      local span = Span{Emph 'Red door'}
      assert.are_equal(
        Span{Emph 'Paint it Black'},
        span:walk{
          Inlines = function (inlns)
            if Span(inlns) == Span('Paint it') then
              return inlns .. {Space(), 'Black'}
            end
          end,
          Str = function (str)
            if str == Str 'Red' then
              return 'Paint'
            elseif str == Str 'door' then
              return 'it'
            end
          end
        }
      )
    end),
    test('uses order Inline -> Inlines -> Block -> Blocks', function ()
      local names = List{}
      Note{Para 'Human After All', CodeBlock 'Alive 2007'}:walk{
        Blocks = function (_)
          names:insert('Blocks')
        end,
        Block = function (b)
          names:insert(b.t)
        end,
        Inline = function (i)
          names:insert(i.t)
        end,
        Inlines = function (_)
          names:insert('Inlines')
        end,
      }
      assert.are_equal(
        'Str, Space, Str, Space, Str, Inlines, Para, CodeBlock, Blocks',
        table.concat(names, ', ')
      )
    end),
  },

  group 'marshalling' {
    test('bare strings become Str values', function ()
      assert.are_equal(Inlines{'a'}, Inlines{Str 'a'})
    end),

    group '__toinline metamethod' {
      test('metamethod __toinline is called when available', function ()
        local function toinline (t)
          return Code(t.code, {id = t.id, class = t.class})
        end
        local my_code = setmetatable(
          {code = 'open access', id='opn'},
          {__toinline = toinline}
        )
        assert.are_equal(
          Inlines{Code('open access', {'opn'})},
          Inlines{my_code}
        )
      end),

      test("metafield is ignored if it's not a function", function ()
        local bad_inline = setmetatable({'a'}, {__toinline = true})
        assert.are_equal(
          Inlines({'a'}),
          Inlines(bad_inline)
        )
      end),

      test("non-Inline return values are ignored", function ()
        local function toinline ()
          return "not an inline"
        end
        local bad_inline = setmetatable({'a'}, {__toinline = toinline})
        assert.are_equal(
          Inlines({'a'}),
          Inlines(bad_inline)
        )
      end),
    }
  }
}
