#!/usr/bin/env ruby
require 'rubygems'
$:.unshift(File.expand_path("../../lib", __FILE__))
require "bundler/setup"
require 'logger'
require 'json/ld'
require 'linkeddata'
require 'rdf/isomorphic'
require File.expand_path("../../spec/spec_helper", __FILE__)
require File.expand_path("../../spec/suite_helper", __FILE__)
require 'getoptlong'

ASSERTOR = "http://greggkellogg.net/foaf#me"
RUN_TIME = Time.now

MultiJson.use(:json_gem) # because of differences in parsing with OJ

def earl_preamble(options)
  options[:output].write File.read(File.expand_path("../../etc/doap.ttl", __FILE__))
  options[:output].puts %(
<https://rubygems.org/gems/json-ld> doap:release [
  doap:name "json-ld-#{JSON::LD::VERSION}";
  doap:revision "#{JSON::LD::VERSION}";
  doap:created "#{File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d')}"^^xsd:date;
] .
<> foaf:primaryTopic <https://rubygems.org/gems/json-ld>;
  dc:issued "#{RUN_TIME.xmlschema}"^^xsd:dateTime;
  foaf:maker <#{ASSERTOR}> .

<#{ASSERTOR}> a earl:Assertor;
  foaf:title "Implementor" .
)
end

def compare_results(tc, result, expected)
  if tc.evaluationTest?
    if tc.testType == "jld:ToRDFTest"
      expected.equivalent_graph?(result) ? "passed": "failed"
    elsif tc.options[:ordered]
      expected == result ? "passed": "failed"
    else
      if !expected.equivalent_jsonld?(result)
        "failed"
      elsif result.to_s.include?('@context')
        exp_expected = JSON::LD::API.expand(expected, **tc.options.merge(logger: false))
        exp_result = JSON::LD::API.expand(result, **tc.options.merge(logger: false))
        exp_expected.equivalent_jsonld?(exp_result) ? "passed" : "failed"
      else
        "passed"
      end
    end
  else
    result.nil? ? "failed": "passed"
  end
end

def run_tc(man, tc, options)
  tc.options[:logger] = options[:logger]
  tc.options[:documentLoader] ||= Fixtures::SuiteTest.method(:documentLoader)
  tc.options[:lowercaseLanguage] = true

  if tc.options[:specVersion] == "json-ld-1.0"
    STDERR.puts "skip #{tc.property('input')} (1.0 test)" if options[:verbose]
    return
  end

  STDERR.write "run #{tc.property('input')}"

  if options[:verbose]
    puts "\nTestCase: #{tc.inspect}"
    puts "\nInput:\n" + tc.input
    puts "\nContext:\n" + tc.context if tc.context
    puts "\nFrame:\n" + tc.frame if tc.frame
    puts "\nExpected:\n" + tc.expect if tc.expect && tc.positiveTest?
    puts "\nExpected:\n" + tc.expectErrorCode if tc.negativeTest?
  end

  output = ""
  begin
    puts "open #{tc.input_loc}" if options[:verbose]

    result = case tc.testType
    when 'jld:CompactTest'
      output = JSON::LD::API.compact(tc.input_loc, tc.context_json['@context'], validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:ExpandTest'
      # MultiJson use OJ, by default, which doesn't handle native numbers the same as the JSON gem.
      output = JSON::LD::API.expand(tc.input_loc, validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FlattenTest'
      output = JSON::LD::API.flatten(tc.input_loc, (tc.context_json['@context'] if tc.context_loc), validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FrameTest'
      output = JSON::LD::API.frame(tc.input_loc, tc.frame_loc, validate: true, **tc.options)
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:FromRDFTest'
      repo = RDF::Repository.load(tc.input_loc, format: :nquads, rdfstar: tc.options[:rdfstar])
      output = if options[:stream]
        JSON.parse(JSON::LD::Writer.buffer(stream: true, validate: true, **tc.options) {|w| w << repo})
      else
        JSON::LD::API.fromRdf(repo, validate: true, **tc.options)
      end
      expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest?
      compare_results(tc, output, expected)
    when 'jld:ToRDFTest'
      output = RDF::Repository.new.extend(RDF::Isomorphic)
      if options[:stream]
        JSON::LD::Reader.open(tc.input_loc, stream: true, **tc.options.merge(logger: false)) do |statement|
          output << statement
        end
      else
        JSON::LD::API.toRdf(tc.input_loc, **tc.options).map do |statement|
          output << statement
        end
      end

      if tc.evaluationTest? && tc.positiveTest?
        begin
          if tc.options[:produceGeneralizedRdf]
            quads = JSON::LD::API.toRdf(tc.input_loc, **tc.options.merge(validate: false)).map do |statement|
              # Not really RDF, try different test method
              tc.to_quad(statement)
            end
            output = quads.sort.uniq.join("")
            # toRdf/e075 is hard to test, but verified manually
            output == tc.expect ? 'passed' : (tc.input_loc.include?('e075') ? 'passed' : 'failed')
          else
            expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, rdfstar: tc.options[:rdfstar], validate: false, logger: [])
            output.isomorphic?(expected) ? 'passed' : 'failed'
          end
        rescue RDF::ReaderError, JSON::LD::JsonLdError
          quads = JSON::LD::API.toRdf(tc.input_loc, rdfstar: tc.options[:rdfstar], **tc.options.merge(validate: false)).map do |statement|
            # Not really RDF, try different test method
            tc.to_quad(statement)
          end

          # FIXME: toRDF is outputing duplicate quads
          output = quads.sort.uniq.join("")
          output == tc.expect ? 'passed' : 'failed'
        end
      else
        output.count > 0 ? 'passed' : 'failed'
      end
    end || "untested"

    output = output.dump(:nquads, validate: false) rescue output.to_s if output.is_a?(RDF::Enumerable)
    puts "\nOutput:\n" + (tc.testType == 'jld:ToRDFTest' ? output : output.to_json(JSON::LD::JSON_STATE)) if !tc.syntaxTest? && options[:verbose]

    result = result ? 'failed' : 'passed' unless tc.positiveTest?
    options[:results][result] ||= 0
    options[:results][result] += 1
  rescue Interrupt
    $stderr.puts "(interrupt)"
    exit 1
  rescue StandardError => e
    result = if tc.positiveTest?
      STDERR.puts "#{" exception" unless options[:quiet]}: #{e}"
      if options[:quiet] || !options[:verbose]
        options[:results]['failed'] ||= 0
        options[:results]['failed'] += 1
      else
        raise
      end
      "failed"
    else
      if e.message.include?(tc.property('expectErrorCode'))
        options[:results]['passed'] ||= 0
        options[:results]['passed'] += 1
        "passed"
      else
        STDERR.puts("Expected exception: '#{tc.property('expectErrorCode')}' not '#{e}'") unless options[:quiet]
        options[:results]['failed'] ||= 0
        options[:results]['failed'] += 1
        "failed"
      end
    end
  end
  
  #options[:output].puts("\nOutput:\n" + output) unless options[:quiet]

  if options[:earl]
    options[:output].puts %{
[ a earl:Assertion;
  earl:assertedBy <#{ASSERTOR}>;
  earl:subject <https://rubygems.org/gems/json-ld>;
  earl:test <#{man}#{tc.id}>;
  earl:result [
    a earl:TestResult;
    earl:outcome earl:#{result};
    dc:date "#{RUN_TIME.xmlschema}"^^xsd:dateTime];
  earl:mode earl:automatic ] .
}
  end

  puts "#{" test result:" unless options[:quiet]} #{result}"
end

logger = Logger.new(STDERR)
logger.level = Logger::WARN
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}

options = {
  output: STDOUT,
  results: {},
  logger: logger
}

opts = GetoptLong.new(
  ["--help", "-?", GetoptLong::NO_ARGUMENT],
  ["--debug", GetoptLong::NO_ARGUMENT],
  ["--earl", GetoptLong::NO_ARGUMENT],
  ["--quiet", "-q", GetoptLong::NO_ARGUMENT],
  ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT],
  ["--stream",   GetoptLong::NO_ARGUMENT],
  ["--verbose", "-v", GetoptLong::NO_ARGUMENT]
)

def help(options)
  puts "Usage: #{$0} [options] [test-number ...]"
  puts "Options:"
  puts "      --debug:              Display detailed debug output"
  puts "      --earl:               Generate EARL report"
  puts "      --quiet:              Minimal output"
  puts "      --output:             Output to specified file"
  puts "      --stream:             Use streaming RDF reader/writer"
  puts "      --verbose:            Verbose processing"
  puts "      --help,-?:            This message"
  exit(0)
end

opts.each do |opt, arg|
  case opt
  when '--help'             then help(options)
  when '--debug'            then logger.level = Logger::DEBUG
  when '--earl'
    options[:quiet] = options[:earl] = true
    logger.level = Logger::FATAL
  when '--output'           then options[:output] = File.open(arg, "w")
  when '--quiet'
    options[:quiet] = true
    logger.level = Logger::FATAL
  when '--stream'           then options[:stream] = true
  when '--verbose'          then options[:verbose] = true
  end
end

manifests = if options[:stream]
  ["#{Fixtures::SuiteTest::STREAM_SUITE}stream-toRdf-manifest.jsonld"]
else
  %w(expand compact flatten fromRdf html remote-doc toRdf).map do |man|
    "#{Fixtures::SuiteTest::SUITE}#{man}-manifest.jsonld"
  end +
  ["#{Fixtures::SuiteTest::FRAME_SUITE}frame-manifest.jsonld"] +
  %w{expand compact flatten fromRdf toRdf}.map do |man|
    "#{Fixtures::SuiteTest::STAR_SUITE}#{man}-manifest.jsonld"
  end
end

earl_preamble(options) if options[:earl]

manifests.each do |man|
  Fixtures::SuiteTest::Manifest.open(man) do |m|
    m.entries.each do |tc|
      next unless ARGV.empty? || ARGV.any? {|n| tc.property('@id').match(/#{n}/) || tc.property('input').match(/#{n}/)}
      options = {stream: true}.merge(options) if man.include?('stream')
      run_tc(man.sub(".jsonld", ""), tc, options)
    end
  end
end

options[:results].each {|k, v| puts "#{k}: #{v}"}
