# Modified work Copyright (c) 2017-2018 Science and Technology
# Facilities Council
# Original work Copyright (c) 1999-2008 Pearu Peterson
#
# All rights reserved.
#
# Modifications made as part of the fparser project are distributed
# under the following license:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# --------------------------------------------------------------------

# The original software (in the f2py project) was distributed under
# the following license:

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:

#   a. Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#   b. Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#   c. Neither the name of the F2PY project nor the names of its
#      contributors may be used to endorse or promote products derived from
#      this software without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.

"""
Test fparser support for parsing select-case and select-type blocks

"""

import pytest
import fparser.common.sourceinfo


def test_case():
    """Basic tests for parsing of individual case statements"""
    from fparser.one.tests.test_parser import parse
    from fparser.one.block_statements import Case

    assert parse(Case, "case (1)") == "CASE ( 1 )"
    assert parse(Case, "case (1:)") == "CASE ( 1 : )"
    assert parse(Case, "case (:1)") == "CASE ( : 1 )"
    assert parse(Case, "case (1:2)") == "CASE ( 1 : 2 )"
    assert parse(Case, "case (a(1,2))") == "CASE ( a(1,2) )"
    assert parse(Case, 'case ("ab")') == 'CASE ( "ab" )'
    assert parse(Case, "case default") == "CASE DEFAULT"
    assert parse(Case, "case (1:2 ,3:4)") == "CASE ( 1 : 2, 3 : 4 )"
    assert parse(Case, "case (a(1,:):)") == "CASE ( a(1,:) : )"
    assert parse(Case, "case default") == "CASE DEFAULT"


def test_case_internal_error(monkeypatch, capsys):
    """Check that expected errors are raised when invalid case
    statements are encountered"""
    from fparser.one.block_statements import Case
    from fparser.common.readfortran import FortranStringReader

    reader = FortranStringReader("CASE (yes)")
    reader.set_format(fparser.common.sourceinfo.FortranFormat(True, False))
    item = next(reader)
    stmt = Case(item, item)
    # Monkeypatch our valid Case object so that get_line() now
    # returns something invalid. We have to do it this way
    # because if we started with this text then we wouldn't get
    # past the match() method
    monkeypatch.setattr(stmt.item, "get_line", lambda: "case invalid")
    # Monkeypatch the Case object so that a call to self.warning
    # (which normally results in a call to the logger) gets replaced
    # with a call to print
    monkeypatch.setattr(stmt, "warning", lambda out: print(out))
    stmt.process_item()
    output, _ = capsys.readouterr()
    print(output)
    assert "Internal error when parsing CASE statement" in output


def test_class_internal_error(monkeypatch, capsys):
    """Check that expected errors are raised when invalid CLASS
    statements are encountered"""
    from fparser.one.block_statements import ClassIs
    from fparser.common.readfortran import FortranStringReader

    reader = FortranStringReader("CLASS IS (yes)")
    reader.set_format(fparser.common.sourceinfo.FortranFormat(True, False))
    item = next(reader)
    stmt = ClassIs(item, item)
    # Monkeypatch our valid Case object so that get_line() now
    # returns something invalid. We have to do it this way
    # because if we started with this text then we wouldn't get
    # past the match() method
    monkeypatch.setattr(stmt.item, "get_line", lambda: "class invalid")
    # Monkeypatch the Case object so that a call to self.warning
    # (which normally results in a call to the logger) gets replaced
    # with a call to with a call to print
    monkeypatch.setattr(stmt, "warning", lambda out: print(out))
    stmt.process_item()
    output, _ = capsys.readouterr()
    assert "Internal error when parsing CLASS statement" in output


def test_select_case():
    """Test that fparser correctly recognises select case"""
    from fparser import api

    source_str = """
    subroutine foo
    integer :: iflag = 1
    real    :: aval = 0.0
    select case(iflag)
    case(1)
      aval = 1.0
    case default
      aval = 0.0
    end select
    ! Same again but with more white space
    select   case(iflag)
    case ( 1 )
      aval = 1.0
    case  default
      aval = 0.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False, ignore_comments=True)
    assert tree
    select_list = []
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectCase):
            select_list.append(statement)
    assert len(select_list) == 2
    for statement in select_list:
        assert isinstance(statement, fparser.one.block_statements.SelectCase)
        assert isinstance(statement.content[0], fparser.one.statements.Case)
        assert isinstance(statement.content[2], fparser.one.statements.Case)
        assert isinstance(statement.content[3], fparser.one.statements.Assignment)
    gen = str(tree)
    print(gen)
    assert "SELECT CASE ( iflag )" in gen


@pytest.mark.xfail(reason="fparser does not work with named select statements")
def test_named_select_case():
    """Test that fparser correctly recognises a named select case"""
    from fparser import api

    source_str = """
    subroutine foo
    integer :: iflag = 1
    real    :: aval = 0.0
    incase: select case(iflag)
    case(1) incase
      aval = 1.0
    case default   incase
      aval = 0.0
    end select  incase
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    select_list = []
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectCase):
            select_list.append(statement)
    assert len(select_list) == 2
    for statement in select_list:
        assert isinstance(statement, fparser.one.block_statements.SelectCase)
        assert isinstance(statement.content[0], fparser.one.statements.Case)
        assert isinstance(statement.content[2], fparser.one.statements.Case)
        assert isinstance(statement.content[3], fparser.one.statements.Assignment)
    gen = str(tree)
    print(gen)
    assert "incase: SELECT CASE ( iflag )" in gen


def test_select_case_brackets():
    """Test that fparser correctly parses a select case involving
    parentheses"""
    from fparser import api

    source_str = """
    subroutine foo
    integer :: iflag(2) = 1
    real    :: aval = 0.0
    select case(iflag(1))
    case(1)
      aval = 1.0
    case default
      aval = 0.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    statement = None  # Keep pylint happy
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectCase):
            break
    assert isinstance(statement, fparser.one.block_statements.SelectCase)
    assert isinstance(statement.content[0], fparser.one.statements.Case)
    assert isinstance(statement.content[2], fparser.one.statements.Case)
    assert isinstance(statement.content[3], fparser.one.statements.Assignment)
    gen = str(tree)
    print(gen)
    assert "SELECT CASE ( iflag(1) )" in gen


def test_select_type():
    """Test that fparser correctly recognises select type"""
    from fparser import api

    source_str = """
    subroutine foo(an_object)
    class(*) :: an_object
    real    :: aval = 0.0
    select type(an_object)
    type is (some_type)
      aval = 1.0
    class is (some_class)
      aval = 0.0
    class default
      aval = -1.0
    end select
    ! Same again but with more white space
    select   type ( an_object )
    type  is  ( some_type(i), some_other_type )
      aval = 1.0
    class  is  ( some_class(1), some_other_class )
      aval = 0.0
    class   default
      aval = -1.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    select_list = []
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectType):
            select_list.append(statement)
    assert len(select_list) == 2
    for statement in select_list:
        assert isinstance(statement, fparser.one.block_statements.SelectType)
        assert isinstance(statement.content[0], fparser.one.statements.TypeIs)
        assert isinstance(statement.content[2], fparser.one.statements.ClassIs)
        assert isinstance(statement.content[3], fparser.one.statements.Assignment)
        assert isinstance(statement.content[4], fparser.one.statements.ClassIs)
    gen = str(tree)
    print(gen)
    assert "SELECT TYPE ( an_object )" in gen
    assert "TYPE IS ( some_type )" in gen
    assert "TYPE IS ( some_type(i), some_other_type )" in gen
    assert "CLASS IS ( some_class )" in gen
    assert "CLASS DEFAULT" in gen
    assert "CLASS IS ( some_class(1), some_other_class )" in gen


def test_type_is_process_item(monkeypatch, capsys):
    """Test error condition raised in TypeIs.process_item() method"""
    from fparser import api

    source_str = """
    subroutine foo(an_object)
    class(*) :: an_object
    real    :: aval = 0.0
    select type(an_object)
    type is (some_type)
      aval = 1.0
    class is (some_class)
      aval = 0.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    statement = None  # Keeps pylint happy
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectType):
            break
    assert isinstance(statement, fparser.one.block_statements.SelectType)
    assert isinstance(statement.content[0], fparser.one.statements.TypeIs)
    typeis = statement.content[0]
    typeis.parent.name = "not_a_name"
    monkeypatch.setattr(typeis.item, "get_line", lambda: "type is (blah): wrong_name")
    # Monkeypatch the typeis object so that a call to self.warning
    # (which normally results in a call to the logger) gets replaced
    # with a call to with a call to print
    monkeypatch.setattr(typeis, "warning", lambda out: print(out))
    typeis.process_item()
    output, _ = capsys.readouterr()
    print(output)
    assert "expected type-is-construct-name 'not_a_name' but got " in output


def test_type_is_to_fortran():
    """Test error condition raised in TypeIs.to_fortran() method"""
    from fparser import api
    from fparser.common.utils import ParseError

    source_str = """
    subroutine foo(an_object)
    class(*) :: an_object
    real    :: aval = 0.0
    select type(an_object)
    type is (some_type)
      aval = 1.0
    class is (some_class)
      aval = 0.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    statement = None  # Keeps pylint happy
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectType):
            break
    assert isinstance(statement, fparser.one.block_statements.SelectType)
    assert isinstance(statement.content[0], fparser.one.statements.TypeIs)
    typeis = statement.content[0]
    typeis.name = "some_name"
    fort = typeis.tofortran()
    assert "TYPE IS ( some_type ) some_name" in fort
    # Now break the internal state and check for expected exception
    typeis.items = None
    with pytest.raises(ParseError) as excinfo:
        _ = typeis.tofortran()
    assert "TYPE IS construct must have arguments" in str(excinfo.value)


def test_class_is_process_item(monkeypatch, capsys):
    """Test error condition raised in ClassIs.process_item() method"""
    from fparser import api

    source_str = """
    subroutine foo(an_object)
    class(*) :: an_object
    real    :: aval = 0.0
    select type(an_object)
    class is (some_class)
      aval = 0.0
    class default
      aval = -1.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    statement = None  # Keeps pylint happy
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectType):
            break
    assert isinstance(statement, fparser.one.block_statements.SelectType)
    assert isinstance(statement.content[0], fparser.one.statements.ClassIs)
    clsis = statement.content[0]
    clsis.parent.name = "not_a_name"
    monkeypatch.setattr(clsis.item, "get_line", lambda: "class is (blah): wrong_name")
    # Monkeypatch the typeis object so that a call to self.warning
    # (which normally results in a call to the logger) gets replaced
    # with a call to with a call to print
    monkeypatch.setattr(clsis, "warning", lambda out: print(out))
    clsis.process_item()
    output, _ = capsys.readouterr()
    print(output)
    assert "expected class-construct-name 'not_a_name' but got " in output


def test_class_is_to_fortran():
    """Test ClassIs.to_fortran() method"""
    from fparser import api

    source_str = """
    subroutine foo(an_object)
    class(*) :: an_object
    real    :: aval = 0.0
    select type(an_object)
    class is (some_class)
      aval = 0.0
    end select
    end subroutine foo
    """
    tree = api.parse(source_str, isfree=True, isstrict=False)
    assert tree
    statement = None  # Keeps pylint happy
    for statement in tree.content[0].content:
        if isinstance(statement, fparser.one.block_statements.SelectType):
            break
    assert isinstance(statement, fparser.one.block_statements.SelectType)
    assert isinstance(statement.content[0], fparser.one.statements.ClassIs)
    clsis = statement.content[0]
    clsis.name = "some_name"
    fort = clsis.tofortran()
    assert "CLASS IS ( some_class ) some_name" in fort
