# Author:: Couchbase <info@couchbase.com>
# Copyright:: 2012 Couchbase, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'minitest/autorun'
require 'couchbase'
require 'couchbase/model'

require 'socket'
require 'open-uri'

class CouchbaseServer
  attr_accessor :host, :port, :num_nodes, :buckets_spec

  def real?
    true
  end

  def initialize(params = {})
    @host, @port = ENV['COUCHBASE_SERVER'].split(':')
    @port = @port.to_i

    if @host.nil? || @host.empty? || @port == 0
      raise ArgumentError, 'Check COUCHBASE_SERVER variable. It should be hostname:port'
    end

    @config = Yajl::Parser.parse(open("http://#{@host}:#{@port}/pools/default"))
    @num_nodes = @config['nodes'].size
    @buckets_spec = params[:buckets_spec] || 'default:'  # "default:,protected:secret,cache::memcache"
  end

  def start
    # flush all buckets
    @buckets_spec.split(',') do |bucket|
      name, password, _ = bucket.split(':')
      connection = Couchbase.new(:hostname => @host,
                                 :port => @port,
                                 :username => name,
                                 :bucket => name,
                                 :password => password)
      connection.flush
    end
  end
  def stop; end
end

class CouchbaseMock
  Monitor = Struct.new(:pid, :client, :socket, :port)

  attr_accessor :host, :port, :buckets_spec, :num_nodes, :num_vbuckets

  def real?
    false
  end

  def initialize(params = {})
    @host = '127.0.0.1'
    @port = 0
    @num_nodes = 10
    @num_vbuckets = 4096
    @buckets_spec = 'default:'  # "default:,protected:secret,cache::memcache"
    params.each do |key, value|
      send("#{key}=", value)
    end
    yield self if block_given?
    if @num_vbuckets < 1 || (@num_vbuckets & (@num_vbuckets - 1) != 0)
      raise ArgumentError, 'Number of vbuckets should be a power of two and greater than zero'
    end
  end

  def start
    @monitor = Monitor.new
    @monitor.socket = TCPServer.new(nil, 0)
    @monitor.socket.listen(10)
    _, @monitor.port, _, _ = @monitor.socket.addr
    trap('CLD') do
      puts 'CouchbaseMock.jar died unexpectedly during startup'
      exit(1)
    end
    @monitor.pid = fork
    if @monitor.pid.nil?
      rc = exec(command_line("--harakiri-monitor=:#{@monitor.port}"))
    else
      trap('CLD', 'SIG_DFL')
      @monitor.client, _ = @monitor.socket.accept
      @port = @monitor.client.recv(100).to_i
    end
  end

  def stop
    @monitor.client.close
    @monitor.socket.close
    Process.kill('TERM', @monitor.pid)
    Process.wait(@monitor.pid)
  end

  def failover_node(index, bucket = 'default')
    @monitor.client.send("failover,#{index},#{bucket}", 0)
  end

  def respawn_node(index, bucket = 'default')
    @monitor.client.send("respawn,#{index},#{bucket}", 0)
  end

  protected

  def command_line(extra = nil)
    cmd = "java -jar #{File.dirname(__FILE__)}/CouchbaseMock.jar"
    cmd << " --host #{@host}" if @host
    cmd << " --port #{@port}" if @port
    cmd << " --nodes #{@num_nodes}" if @num_nodes
    cmd << " --vbuckets #{@num_vbuckets}" if @num_vbuckets
    cmd << " --buckets #{@buckets_spec}" if @buckets_spec
    cmd << " #{extra}"
    cmd
  end
end

class MiniTest::Unit::TestCase

  def start_mock(params = {})
    mock = nil
    if ENV['COUCHBASE_SERVER']
      mock = CouchbaseServer.new(params)
      if (params[:port] && mock.port != params[:port]) ||
        (params[:host] && mock.host != params[:host]) ||
        mock.buckets_spec != 'default:'
        skip("Unable to configure real cluster. Requested config is: #{params.inspect}")
      end
    else
      mock = CouchbaseMock.new(params)
    end
    mock.start
    mock
  end

  def stop_mock(mock)
    assert(mock)
    mock.stop
  end

  def with_mock(params = {})
    mock = nil
    if block_given?
      mock = start_mock(params)
      yield mock
    end
  ensure
    stop_mock(mock) if mock
  end

  def uniq_id(*suffixes)
    test_id = [caller.first[/.*[` ](.*)'/, 1], suffixes].compact.join('_')
    @ids ||= {}
    @ids[test_id] ||= Time.now.to_f
    [test_id, @ids[test_id]].join('_')
  end
end
