require 'test/unit'
require 'protobuf/compiler/compiler'

class CompilerTest < Test::Unit::TestCase
  def save_const(parent_class, const_name)
    if parent_class.const_defined?(const_name)
      saved = parent_class.const_get(const_name)
      parent_class.__send__(:remove_const, const_name)
    end

    yield

    if saved
      parent_class.__send__(:remove_const, const_name) if parent_class.const_defined?(const_name)
      parent_class.const_set(const_name, saved)
    end
  end

  # Issue 12:    Parse error on float default value
  def test_compile_float_default
    assert_compile_proto(<<-EOS, 'test/proto/float_default.proto')
### Generated by rprotoc. DO NOT EDIT!
### <proto file: test/proto/float_default.proto>
# message M {  
#   optional float f = 1 [default = 4.2];  
#   optional float g = 2 [default = -4.2];  
#   optional float h = 3 [default = 4352];
#   optional float i = 4 [default = 23145.2 ];
#   optional float j = 5 [default = -5 ];
#   optional float k = 6 [default = +23 ];
#   optional float l = 7 [default = +23.42 ];
# }
# 

require 'protobuf/message/message'
require 'protobuf/message/enum'
require 'protobuf/message/service'
require 'protobuf/message/extend'

class M < ::Protobuf::Message
  defined_in __FILE__
  optional :float, :f, 1, :default => 4.2
  optional :float, :g, 2, :default => -4.2
  optional :float, :h, 3, :default => 4352
  optional :float, :i, 4, :default => 23145.2
  optional :float, :j, 5, :default => -5
  optional :float, :k, 6, :default => 23
  optional :float, :l, 7, :default => 23.42
end
    EOS
  end

  def test_create_message
    save_const(Object, :Tutorial) do
      assert_compile_proto(<<-EOS, 'test/proto/addressbook.proto')
### Generated by rprotoc. DO NOT EDIT!
### <proto file: test/proto/addressbook.proto>
# package tutorial;
# 
# message Person {
#   required string name = 1;
#   required int32 id = 2;
#   optional string email = 3;
# 
#   enum PhoneType {
#     MOBILE = 0;
#     HOME = 1;
#     WORK = 2;
#   }
# 
#   message PhoneNumber {
#     required string number = 1;
#     optional PhoneType type = 2 [default = HOME];
#   }
# 
#   repeated PhoneNumber phone = 4;
#   optional uint32 age = 5 [default = 20];
# 
#   extensions 100 to 200;
# }
# 
# /*
# extend Person {
#   optional int32 age = 100;
# }
# */
# 
# message AddressBook {
#   repeated Person person = 1;
# }
require 'protobuf/message/message'
require 'protobuf/message/enum'
require 'protobuf/message/service'
require 'protobuf/message/extend'

module Tutorial
  
  class Person < ::Protobuf::Message
    defined_in __FILE__
    required :string, :name, 1
    required :int32, :id, 2
    optional :string, :email, 3
    
    class PhoneType < ::Protobuf::Enum
      defined_in __FILE__
      MOBILE = value(:MOBILE, 0)
      HOME = value(:HOME, 1)
      WORK = value(:WORK, 2)
    end
    
    class PhoneNumber < ::Protobuf::Message
      defined_in __FILE__
      required :string, :number, 1
      optional :PhoneType, :type, 2, :default => :HOME
    end
    
    repeated :PhoneNumber, :phone, 4
    optional :uint32, :age, 5, :default => 20
    
    extensions 100..200
  end
  
  class AddressBook < ::Protobuf::Message
    defined_in __FILE__
    repeated :Person, :person, 1
  end
end
      EOS
    end
  end

  def test_create_nested_message
    save_const(Test, :Nested) do
      assert_compile_proto(<<-EOS, 'test/proto/nested.proto')
### Generated by rprotoc. DO NOT EDIT!
### <proto file: test/proto/nested.proto>
# package test.nested;
# 
# message Foo {  
#   message Bar {
#   } 
# }
# message Baaz {  
#   optional Foo.Bar x = 1;
# }

require 'protobuf/message/message'
require 'protobuf/message/enum'
require 'protobuf/message/service'
require 'protobuf/message/extend'

module Test
  module Nested
    class Foo < ::Protobuf::Message
      defined_in __FILE__
      class Bar < ::Protobuf::Message
        defined_in __FILE__
      end
    end
    class Baaz < ::Protobuf::Message
      defined_in __FILE__
      optional :'Foo::Bar', :x, 1
    end
  end
end
      EOS
    end
  end

  def test_nested_message
    save_const(Test, :Nested) do
      file_contents = Protobuf::Compiler.new.create_message('test/proto/nested.proto', '.', '.', false)
      assert_raise(TypeError) {Test::Nested::Baaz.new.x = 1}
      assert_nothing_raised {Test::Nested::Baaz.new.x = Test::Nested::Foo::Bar.new}
    end
  end

  def test_create_rpc
    file_contents = Protobuf::Compiler.new.create_rpc('test/proto/rpc.proto', '.', 'test/proto', false)

    assert_source(<<-EOS, file_contents['test/proto/address_book_service.rb'])
require 'protobuf/rpc/server'
require 'protobuf/rpc/handler'
require 'test/proto/rpc.pb'

class Tutorial::SearchHandler < Protobuf::Rpc::Handler
  request Tutorial::Person
  response Tutorial::AddressBook

  def self.process_request(request, response)
    # TODO: edit this method
  end
end

class Tutorial::AddHandler < Protobuf::Rpc::Handler
  request Tutorial::Person
  response Tutorial::Person

  def self.process_request(request, response)
    # TODO: edit this method
  end
end

class Tutorial::AddressBookService < Protobuf::Rpc::Server
  def setup_handlers
    @handlers = {
      :search => Tutorial::SearchHandler,
      :add => Tutorial::AddHandler,
    }
  end
end
    EOS

    assert_source(<<-EOS, file_contents['test/proto/start_address_book_service'])
#!/usr/bin/env ruby
require 'address_book_service'

Tutorial::AddressBookService.new(:Port => 9999).start
    EOS

    assert_source(<<-EOS, file_contents['test/proto/client_search.rb'])
#!/usr/bin/env ruby
require 'protobuf/rpc/client'
require 'test/proto/rpc.pb'

# build request
request = Tutorial::Person.new
# TODO: setup a request
raise StandardError, 'setup a request'

# create blank response
response = Tutorial::AddressBook.new

# execute rpc
Protobuf::Rpc::Client.new('localhost', 9999).call :search, request, response

# show response
puts response
    EOS

    assert_source(<<-EOS, file_contents['test/proto/client_add.rb'])
#!/usr/bin/env ruby
require 'protobuf/rpc/client'
require 'test/proto/rpc.pb'

# build request
request = Tutorial::Person.new
# TODO: setup a request
raise StandardError, 'setup a request'

# create blank response
response = Tutorial::Person.new

# execute rpc
Protobuf::Rpc::Client.new('localhost', 9999).call :add, request, response

# show response
puts response
    EOS
  end

  def test_create_descriptor
    proto_path = 'test/proto/addressbook.proto'
    visitor = Protobuf::Visitor::CreateDescriptorVisitor.new(proto_path)
    File.open(proto_path) do |file|
      visitor.visit Protobuf::ProtoParser.new.parse(file)
    end
    file_descriptor = visitor.file_descriptor
    assert_equal(proto_path, file_descriptor.name)
    assert_equal('tutorial', file_descriptor.package)

    person_descriptor = file_descriptor.message_type[0]
    assert_equal('Person', person_descriptor.name)
    assert_equal([:name, :id, :email, :phone, :age].size, person_descriptor.field.size)

    name_field_descriptor = person_descriptor.field.find {|d| d.name == 'name'}
    assert_equal(1, name_field_descriptor.number)
    assert_equal(Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING, name_field_descriptor.type)
    assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED, name_field_descriptor.label)
    assert_equal('string', name_field_descriptor.type_name)

    phone_field_descriptor = person_descriptor.field.find {|d| d.name == 'phone'}
    assert_equal(4, phone_field_descriptor.number)
    assert_equal(0, phone_field_descriptor.type) #TODO: is this right?
    assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED, phone_field_descriptor.label)
    assert_equal('PhoneNumber', phone_field_descriptor.type_name)

    age_field_descriptor = person_descriptor.field.find {|d| d.name == 'age'}
    assert_equal(5, age_field_descriptor.number)
    assert_equal(Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32, age_field_descriptor.type)
    assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL, age_field_descriptor.label)
    assert_equal('uint32', age_field_descriptor.type_name)
    assert_equal('20', age_field_descriptor.default_value)

    phone_type_descriptor = person_descriptor.enum_type.first
    assert_equal('PhoneType', phone_type_descriptor.name)
    assert_equal(3, phone_type_descriptor.value.size)

    phone_type_home_descriptor = phone_type_descriptor.value.find {|d| d.name == 'HOME'}
    assert_equal('HOME', phone_type_home_descriptor.name)
    assert_equal(1, phone_type_home_descriptor.number)

    extensions_descriptor = person_descriptor.extension_range.first
    assert_equal(100, extensions_descriptor.start)
    assert_equal(200, extensions_descriptor.end)

    phone_number_descriptor = person_descriptor.nested_type.first
    assert_equal('PhoneNumber', phone_number_descriptor.name)
    assert_equal([:number, :type].size, phone_number_descriptor.field.size)

    #TODO: test extend
    #extend_person_descriptor = ??
    #assert_equal extend_person_descriptor

    addressbook_descriptor = file_descriptor.message_type[1]
    assert_equal('AddressBook', addressbook_descriptor.name)
  end

  def test_collision
    assert_raise(Protobuf::TagCollisionError) do require 'test/proto/collision.pb' end
    assert_raise(Protobuf::TagCollisionError) do
      Protobuf::Compiler.new.create_message('test/proto/collision.proto', '.', '.', false)
    end
  end

  def test_ext_collision
    assert_raise(Protobuf::TagCollisionError) do require 'test/proto/ext_collision.pb' end
    assert_raise(Protobuf::TagCollisionError) do
      Protobuf::Compiler.new.create_message('test/proto/ext_collision.proto', '.', '.', false)
    end
  end

  def test_ext_range
    assert_raise(RangeError) do require 'test/proto/ext_range.pb' end
    assert_raise(RangeError) do
      Protobuf::Compiler.new.create_message('test/proto/ext_range.proto', '.', '.', false)
    end
  end

  def assert_compile_proto(ideal, filename)
    assert_equal(ideal.gsub(/^\s*\n/, '').strip, Protobuf::Compiler.new.create_message(filename, '.', '.', false).gsub(/^\s*\n/, '').strip)
  end

  def assert_source(ideal, real)
    assert_equal(ideal.strip.gsub(/^\s*\n/, '').gsub(/\s+\n/, "\n"), real.strip.gsub(/^\s*\n/, '').gsub(/\s+\n/, "\n"))
  end
end
