# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os.path
import unittest
import fnmatch

import SCons.Scanner.IDL
import SCons.Tool.JavaCommon

import TestSCons

# Adding trace=trace to any of the parse_jave() calls below will cause
# the parser to spit out trace messages of the tokens it sees and the
# attendant transitions.

def trace(token, newstate):
    from SCons.Debug import Trace
    statename = newstate.__class__.__name__
    Trace('token = %s, state = %s\n' % (repr(token), statename))

class parse_javaTestCase(unittest.TestCase):

    def test_bare_bones(self):
        """Test a bare-bones class"""

        input = """\
package com.sub.bar;

public class Foo
{

     public static void main(String[] args)
     {

        /* This tests a former bug where strings would eat later code. */
        String hello1 = new String("Hello, world!");

     }

}
"""
        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir == os.path.join('com', 'sub', 'bar'), pkg_dir
        assert classes == ['Foo'], classes


    def test_file_parser(self):
        """Test the file parser"""
        input = """\
package com.sub.bar;

public class Foo
{
     public static void main(String[] args)
     {
        /* This tests that unicde is handled . */
        String hello1 = new String("ఎత్తువెడల్పు");
     }
}
"""
        file_name = 'test_file_parser.java'
        with open(file_name, 'w', encoding='UTF-8') as jf:
            print(input, file=jf)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java_file(file_name)
        if os.path.exists(file_name):
            os.remove(file_name)
        assert pkg_dir == os.path.join('com', 'sub', 'bar'), pkg_dir
        assert classes == ['Foo'], classes


    def test_dollar_sign(self):
        """Test class names with $ in them"""

        input = """\
public class BadDep {
  public void new$rand () {}
}
"""
        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['BadDep'], classes



    def test_inner_classes(self):
        """Test parsing various forms of inner classes"""

        input = """\
class Empty {
}

interface Listener {
  public void execute();
}

public
class
Test implements Listener {
  class Inner {
    void go() {
      use(new Listener() {
        public void execute() {
          System.out.println("In Inner");
        }
      });
    }
    String s1 = "class A";
    String s2 = "new Listener() { }";
    /* class B */
    /* new Listener() { } */
  }

  class Inner2 {
     Inner2() { Listener l = new Listener(); }
  }

  /* Make sure this class doesn't get interpreted as an inner class of the previous one, when "new" is used in the previous class. */
  class Inner3 {

  }

  public static void main(String[] args) {
    new Test().run();
  }

  void run() {
    use(new Listener() {
      public void execute() {
        use(new Listener( ) {
          public void execute() {
            System.out.println("Inside execute()");
          }
        });
      }
    });

    new Inner().go();
  }

  void use(Listener l) {
    l.execute();
  }
}

class Private {
  void run() {
    new Listener() {
      public void execute() {
      }
    };
  }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        assert pkg_dir is None, pkg_dir
        expect = [
                   'Empty',
                   'Listener',
                   'Test$1',
                   'Test$Inner',
                   'Test$Inner2',
                   'Test$Inner3',
                   'Test$2',
                   'Test$3',
                   'Test',
                   'Private$1',
                   'Private',
                 ]
        assert classes == expect, classes

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.5')
        assert pkg_dir is None, pkg_dir
        expect = [
                   'Empty',
                   'Listener',
                   'Test$Inner$1',
                   'Test$Inner',
                   'Test$Inner2',
                   'Test$Inner3',
                   'Test$1',
                   'Test$1$1',
                   'Test',
                   'Private$1',
                   'Private',
                 ]
        assert classes == expect, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '5')
        assert pkg_dir is None, pkg_dir
        expect = [
                   'Empty',
                   'Listener',
                   'Test$Inner$1',
                   'Test$Inner',
                   'Test$Inner2',
                   'Test$Inner3',
                   'Test$1',
                   'Test$1$1',
                   'Test',
                   'Private$1',
                   'Private',
                 ]
        assert classes == expect, (expect, classes)



    def test_comments(self):
        """Test a class with comments"""

        input = """\
package com.sub.foo;

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.RMISecurityManager;
import java.rmi.server.UnicastRemoteObject;

public class Example1 extends UnicastRemoteObject implements Hello {

    public Example1() throws RemoteException {
        super();
    }

    public String sayHello() {
        return "Hello World!";
    }

    public static void main(String args[]) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }
        // a comment
        try {
            Example1 obj = new Example1();

            Naming.rebind("//myhost/HelloServer", obj);

            System.out.println("HelloServer bound in registry");
        } catch (Exception e) {
            System.out.println("Example1 err: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir == os.path.join('com', 'sub', 'foo'), pkg_dir
        assert classes == ['Example1'], classes


    def test_arrays(self):
        """Test arrays of class instances"""

        input = """\
public class Test {
    MyClass abc = new MyClass();
    MyClass xyz = new MyClass();
    MyClass _array[] = new MyClass[] {
        abc,
        xyz
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['Test'], classes



    def test_backslash(self):
        """Test backslash handling"""

        input = """\
public class MyTabs
{
        private class MyInternal
        {
        }
        private final static String PATH = "images\\\\";
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['MyTabs$MyInternal', 'MyTabs'], classes


    def test_enum(self):
        """Test the Java 1.5 enum keyword"""

        input = """\
package p;
public enum a {}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir == 'p', pkg_dir
        assert classes == ['a'], classes


    def test_anon_classes(self):
        """Test anonymous classes"""

        input = """\
public abstract class TestClass
{
    public void completed()
    {
        new Thread()
        {
        }.start();

        new Thread()
        {
        }.start();
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['TestClass$1', 'TestClass$2', 'TestClass'], classes


    def test_closing_bracket(self):
        """Test finding a closing bracket instead of an anonymous class"""

        input = """\
class TestSCons {
    public static void main(String[] args) {
        Foo[] fooArray = new Foo[] { new Foo() };
    }
}

class Foo { }
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['TestSCons', 'Foo'], classes


    def test_dot_class_attributes(self):
        """Test handling ".class" attributes"""

        input = """\
public class Test extends Object
{
    static {
        Class c = Object[].class;
        Object[] s = new Object[] {};
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert classes == ['Test'], classes

        input = """\
public class A {
    public class B {
        public void F(Object[] o) {
            F(new Object[] {Object[].class});
        }
        public void G(Object[] o) {
            F(new Object[] {});
        }
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input)
        assert pkg_dir is None, pkg_dir
        assert classes == ['A$B', 'A'], classes

    def test_anonymous_classes_with_parentheses(self):
        """Test finding anonymous classes marked by parentheses"""

        input = """\
import java.io.File;

public class Foo {
    public static void main(String[] args) {
        File f = new File(
            new File("a") {
                public String toString() {
                    return "b";
                }
            } to String()
        ) {
            public String toString() {
                return "c";
            }
        };
    }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        assert classes == ['Foo$1', 'Foo$2', 'Foo'], classes

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.5')
        assert classes == ['Foo$1', 'Foo$1$1', 'Foo'], classes

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '6')
        assert classes == ['Foo$1', 'Foo$1$1', 'Foo'], classes



    def test_nested_anonymous_inner_classes(self):
        """Test finding nested anonymous inner classes"""

        input = """\
// import java.util.*;

public class NestedExample
{
        public NestedExample()
        {
                Thread t = new Thread() {
                        public void start()
                        {
                                Thread t = new Thread() {
                                        public void start()
                                        {
                                                try {Thread.sleep(200);}
                                                catch (Exception e) {}
                                        }
                                };
                                while (true)
                                {
                                        try {Thread.sleep(200);}
                                        catch (Exception e) {}
                                }
                        }
                };
        }


        public static void main(String argv[])
        {
                NestedExample e = new NestedExample();
        }
}
"""

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        expect = [ 'NestedExample$1', 'NestedExample$2', 'NestedExample' ]
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.5')
        expect = [ 'NestedExample$1', 'NestedExample$1$1', 'NestedExample' ]
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '6')
        expect = [ 'NestedExample$1', 'NestedExample$1$1', 'NestedExample' ]
        assert expect == classes, (expect, classes)

    def test_lambda_after_new(self):
        """Test lamdas after new"""

        input = """\
// import java.util.*;

public class LamdaExample
{

    public void testFunc (int arg1, String arg2, Runnable lambda){
    }
    public LamdaExample()
    {
        testFunc(
            5,
            new String("test"),
            // Lambda symbol is after new, and used curly braces so
            // we should not parse this as a new class.
            () -> {}
        );
    }


    public static void main(String argv[])
    {
            LamdaExample e = new LamdaExample();
    }
}
"""
        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        expect = [ 'LamdaExample' ]
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.8')
        expect = [ 'LamdaExample' ]
        assert expect == classes, (expect, classes)

    def test_private_inner_class_instantiation(self):
        """Test anonymous inner class generated by private instantiation"""

        input = """\
class test
{
    test()
    {
        super();
        new inner();
    }

    static class inner
    {
        private inner() {}
    }
}
"""

        # This is what we *should* generate, apparently due to the
        # private instantiation of the inner class, but don't today.
        #expect = [ 'test$1', 'test$inner', 'test' ]

        # What our parser currently generates, which doesn't match
        # what the Java compiler actually generates.
        expect = [ 'test$inner', 'test' ]

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.5')
        assert expect == classes, (expect, classes)

    def test_floating_point_numbers(self):
        """Test floating-point numbers in the input stream"""
        input = """
// Broken.java
class Broken
{
  /**
   * Detected.
   */
  Object anonymousInnerOK = new Runnable() { public void run () {} };

  /**
   * Detected.
   */
  class InnerOK { InnerOK () { } }

  {
    System.out.println("a number: " + 1000.0 + "");
  }

  /**
   * Not detected.
   */
  Object anonymousInnerBAD = new Runnable() { public void run () {} };

  /**
   * Not detected.
   */
  class InnerBAD { InnerBAD () { } }
}
"""

        expect = ['Broken$1', 'Broken$InnerOK', 'Broken$2', 'Broken$InnerBAD', 'Broken']

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.4')
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.5')
        assert expect == classes, (expect, classes)


    def test_genercis(self):
        """Test that generics don't interfere with detecting anonymous classes"""

        input = """\
import java.util.Date;
import java.util.Comparator;

public class Foo
{
  public void foo()
  {
    Comparator<Date> comp = new Comparator<Date>()
      {
        static final long serialVersionUID = 1L;
        public int compare(Date lhs, Date rhs)
        {
          return 0;
        }
      };
  }
}
"""

        expect = [ 'Foo$1', 'Foo' ]

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.6')
        assert expect == classes, (expect, classes)

        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '6')
        assert expect == classes, (expect, classes)


    def test_in_function_class_declaration(self):
        """
        Test that implementing a class in a function call doesn't confuse SCons.
        """

        input = """
package com.Matthew;

public class AnonDemo {

    public static void main(String[] args) {
        new AnonDemo().execute();
    }

    public void execute() {
        Foo bar = new Foo(new Foo() {
            @Override
            public int getX() { return this.x; }
        }) {
            @Override
            public int getX() { return this.x; }
        };
    }

    public abstract class Foo {
        public int x;
        public abstract int getX();

        public Foo(Foo f) {
            this.x = f.x;
        }

        public Foo() {}
    }
}
"""
        expect = ['AnonDemo$1',
                  'AnonDemo$2',
                  'AnonDemo$Foo',
                  'AnonDemo']
        pkg_dir, classes = SCons.Tool.JavaCommon.parse_java(input, '1.8')
        assert expect == classes, (expect, classes)

    def test_jdk_globs(self):
        """
        Verify that the java path globs work with specific examples.
        :return:
        """
        from SCons.Tool.JavaCommon import (
            java_linux_include_dirs_glob,
            java_linux_version_include_dirs_glob,
            java_win32_dir_glob,
            java_win32_version_dir_glob,
            java_macos_include_dir_glob,
            java_macos_version_include_dir_glob,
        )

        # Test windows globs
        win_java_dirs = [
            (r'C:/Program Files/Java/jdk1.8.0_201/bin', '1.8.0'),
            (r'C:/Program Files/Java/jdk-11.0.2/bin', '11.0.2'),
            (r'C:/Program Files/AdoptOpenJDK/jdk-16.0.1.9-hotspot/bin', '16.0.1'),
            (r'C:/Program Files/Microsoft/jdk-17.0.0.35-hotspot/bin', '17.0.0'),
            (r'C:/Program Files/OpenJDK/openjdk-11.0.13_8/bin', '11.0.13'),
            (r'C:/Program Files/RedHat/java-1.8.0-openjdk-1.8.0.312-1/bin', '1.8.0'),
            (r'C:/Program Files/RedHat/java-11-openjdk-11.0.13-1/bin', '11.0.13'),
        ]

        for (wjd, version) in win_java_dirs:
            if not fnmatch.fnmatch(wjd, java_win32_dir_glob):
                self.fail(
                    "Didn't properly match %s with pattern %s"
                    % (wjd, java_win32_dir_glob)
                )
            if not fnmatch.fnmatch(wjd, java_win32_version_dir_glob % version):
                self.fail(
                    "Didn't properly match %s with version (%s) specific pattern %s"
                    % (wjd, version, java_win32_version_dir_glob % version)
                )

        non_win_java_include_dirs = [
            (
                '/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el7_5.x86_64/include',
                '1.8.0',
            ),
            ('/usr/lib/jvm/java-1.8.0-openjdk-amd64/include', '1.8.0'),
            ('/usr/lib/jvm/java-8-openjdk-amd64/include', '8'),
        ]

        # Test non-windows/non-macos globs
        for (wjd, version) in non_win_java_include_dirs:
            match = False
            globs_tried = []
            for jlig in java_linux_include_dirs_glob:
                globs_tried.append(jlig)

                if fnmatch.fnmatch(wjd, jlig):
                    match = True
                    break

            if not match:
                self.fail(
                    "Didn't properly match %s with pattern %s" % (wjd, globs_tried)
                )

            match = False
            globs_tried = []
            for jlvig in java_linux_version_include_dirs_glob:
                globs_tried.append(jlvig % version)
                if fnmatch.fnmatch(wjd, jlvig % version):
                    match = True
                    break

            if not match:
                self.fail(
                    "Didn't properly match %s with version (%s) specific pattern %s"
                    % (wjd, version, globs_tried)
                )

        # Test macos globs
        # Test windows globs
        macos_java_dirs = [
            # ('/System/Library/Frameworks/JavaVM.framework/Headers/', None),
            (
                '/System/Library/Frameworks/JavaVM.framework/Versions/11.0.2/Headers/',
                '11.0.2',
            ),
        ]

        if not fnmatch.fnmatch(
            '/System/Library/Frameworks/JavaVM.framework/Headers/',
            java_macos_include_dir_glob,
        ):
            self.fail(
                "Didn't properly match %s with pattern %s"
                % (
                    '/System/Library/Frameworks/JavaVM.framework/Headers/',
                    java_macos_include_dir_glob,
                )
            )

        for (wjd, version) in macos_java_dirs:
            if not fnmatch.fnmatch(wjd, java_macos_version_include_dir_glob % version):
                self.fail(
                    "Didn't properly match %s with version (%s) specific pattern %s"
                    % (wjd, version, java_macos_version_include_dir_glob % version)
                )


if __name__ == "__main__":
    unittest.main()

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
