#!/usr/bin/env ruby

# Copyright (C) 2016 Apple Inc. All rights reserved.
#
# 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.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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.


require 'fileutils'
require 'getoptlong'
require 'pathname'
require 'yaml'
require 'find'

THIS_SCRIPT_PATH = Pathname.new(__FILE__).realpath
SCRIPTS_PATH = THIS_SCRIPT_PATH.dirname
WEBKIT_PATH = SCRIPTS_PATH.dirname.dirname
TEST262_YAML_PATH = WEBKIT_PATH + "JSTests/test262.yaml"
TEST262_REVISION_PATH = WEBKIT_PATH + "JSTests/test262/test262-Revision.txt"

raise unless SCRIPTS_PATH.basename.to_s == "Scripts"
raise unless SCRIPTS_PATH.dirname.basename.to_s == "Tools"

def mysys(*cmd)
    printCommandArray(*cmd) if $verbosity >= 1
    raise "Command failed: #{$?.inspect}" unless system(*cmd)
end


def usage
    puts "import-test262-tests [options] <path-to-test262-repository>"
    puts
    puts "-h, --help                   print this help message."
    puts "-f, --failures FAILURES      Supplied file will be used to determine which tests fail."
    puts "                             If a failures file is not provided all tests are assumed to pass."

    exit 1
end

$failures = nil
JS_TEST_REGEXP = /.*\.js/
JS_FIXTURE_REGEXP = /.*(_FIXTURE\.js|_\.js)/
GET_YAML_REGEXP = /\/\*---(?<yaml>.*?)---\*\//m

GetoptLong.new(["--help", "-h", GetoptLong::NO_ARGUMENT],
               ["--failures", "-f", GetoptLong::REQUIRED_ARGUMENT]).each {
    | opt, arg |
    case opt
    when "--help"
        usage
    when "--failures"
        $failures = File.open(Pathname.new(arg)).readlines
    end
}

def didPassForMode(path, strict)
    if $failures
        if strict == :strict
            return $failures.grep(/.*#{path}\.default-strict$/).length == 0
        else
            return $failures.grep(/.*#{path}\.default$/).length == 0
        end
    else
        return true
    end
end

class Test
    attr_writer :failsWithException, :isModule, :isAsync
    attr_accessor :includeFiles, :needsStrict, :needsNonStrict
    attr_reader :path

    def initialize(path)
        @path = path
        @failsWithException = nil
        @includeFiles = []
        @needsStrict = true
        @needsNonStrict = true
        @isModule = false
        @isAsync = false
    end

    def check
        # Throw an exception here since I'm not sure if the test infrastructure works in these cases.
        raise if !@needsStrict and !@needsNonStrict
        raise if @isModule and !@needsNonStrict
    end

    def formatFlags(strict)
        flags = []
        flags << strict if strict == :strict
        flags << :module if @isModule
        flags << :async if @isAsync

        return flags.to_s
    end

    def formatCmdArguments(strict)
        raise if strict == :strict ? !@needsStrict : !@needsNonStrict
        passed = didPassForMode(@path, strict)
        cmd = "runTest262"
        cmd += passed ? " :normal, " : " :fail, "
        cmd += @failsWithException ? @failsWithException.inspect : "\"NoException\""
        cmd += ", #{@includeFiles.inspect}"
        cmd += ", #{formatFlags(strict)}"
    end

    def finalizeIncludes
        if @isAsync
            @includeFiles << "doneprintHandle.js"
        end

        dir = Pathname.new(".")
        @path.dirname.each_filename {
            | part |
            dir += ".."
        }
        dir += "harness"

        @includeFiles.map! { | file | (dir + file).to_s }
    end

end

def processTestFile(path)
    /\/\*---(?<yaml>.*?)---\*\//m =~ File::read(path)

    test = Test.new(path)
    # These should be included by all the tests
    test.includeFiles = ["assert.js", "sta.js"]

    begin
        yamlElements = YAML::load(yaml)
    rescue Exception => e
        puts "Failed to parse YAML for #{path}, threw exception:"
        puts e.inspect
    end
    yamlElements.each {
        | option |
        case option[0]
        when "negative"
            test.failsWithException = option[1].to_s
        when "includes"
            test.includeFiles += option[1]
        when "flags"
            option[1].each {
                | flag |
                case flag
                when "raw", "noStrict"
                    test.needsStrict = false
                when "onlyStrict"
                    test.needsNonStrict = false
                when "module"
                    test.isModule = true
                    test.needsStrict = false
                when "async"
                    test.isAsync = true
                when "generated"
                else
                    raise "Invalid Metadata flag option, #{flag}, when parsing #{$benchmarkDirectory + $benchmark}"
                end
            }
        end
    }

    test.finalizeIncludes
    test.check

    return test
end

class Fixture
    attr_reader :path, :needsNonStrict, :needsStrict

    def initialize(path)
        @path = path
        @needsNonStrict = true
        @needsStrict = false
    end

    def formatCmdArguments(strict)
        return "prepareTest262Fixture"
    end

end

def processFixtureFile(path)
    Fixture.new(path)
end

def processFilesRecursively(path)
    # We only run the run the built-ins, language, and annexB tests.
    # At some point we should add intl402
    tests = []

    Dir.chdir(path) {
        paths = [Pathname.new("test/annexB"), Pathname.new("test/built-ins"), Pathname.new("test/language")]

        paths.each {
            | testPath |
            Find.find(testPath) {
                | file |
                if File.file?(file) and JS_TEST_REGEXP =~ file.to_s
                    path = Pathname.new(file)
                    if JS_FIXTURE_REGEXP =~ file.to_s
                        tests << processFixtureFile(path)
                    else
                        tests << processTestFile(path)
                    end
                end
            }
        }

    }

    return tests
end

def printYAML(tests)
    File.open(TEST262_YAML_PATH, "w+") {
        | outp |
        outp.puts "---"
        tests.each {
            | test |
            if test.needsNonStrict
                outp.puts "- path: test262/" + test.path.to_s
                outp.puts "  cmd: " + test.formatCmdArguments(:nonStrict)
            end
            if test.needsStrict
                outp.puts "- path: test262/" + test.path.to_s
                outp.puts "  cmd: " + test.formatCmdArguments(:strict)
            end
        }

    }
end

def printRevision(test262Path)
    url = "unknown"
    branchname = url
    revision = url
    Dir.chdir(test262Path) {
        branchname = `git rev-parse --abbrev-ref HEAD`.strip
        # Not sure what we should do if they don't have a branch checked out.
        # Getting the url from origin seems reasonable.
        branchname = "origin" if branchname == "HEAD"
        url = `git remote get-url #{branchname}`
        revision = `git rev-parse HEAD`.strip
    }

    File.open(TEST262_REVISION_PATH, "w+") {
        | outp |
        puts "test262 remote url: " + url
        puts "test262 revision: " + revision
        outp.puts "test262 remote url: " + url
        outp.puts "test262 revision: " + revision
        outp.puts
    }
end

test262Path = Pathname.new(ARGV[0])
printRevision(test262Path)
tests = processFilesRecursively(test262Path)
printYAML(tests)
