# Copyright 2016 Google LLC
#
# 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
#
#     https://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/focus"

require "google/cloud/storage"
require "google/cloud/logging"

# class File
#   def self.file? f
#     true
#   end
#   def self.readable? f
#     true
#   end
#   def self.read *args
#     "fake file data"
#   end
# end

module Google
  module Cloud
    module Storage
      def self.stub_new
        define_singleton_method :new do |*args|
          yield *args
        end
      end
      # Create default unmocked methods that will raise if ever called
      def self.new *args
        raise "This code example is not yet mocked"
      end
    end
    module Logging
      def self.stub_new
        define_singleton_method :new do |*args|
          yield *args
        end
      end
      # Create default unmocked methods that will raise if ever called
      def self.new *args
        raise "This code example is not yet mocked"
      end
      class Credentials
        # Override the default constructor
        def self.new *args
          OpenStruct.new(client: OpenStruct.new(updater_proc: Proc.new {}))
        end
      end
    end
    module Core
      module Environment
      # Create default unmocked methods that will raise if ever called
        def self.gce_vm? connection: nil
          raise "This code example is not yet mocked"
        end
        def self.get_metadata_attribute uri, attr_name, connection: nil
          raise "This code example is not yet mocked"
        end
      end
    end
  end
end

def mock_storage
  Google::Cloud::Storage.stub_new do |*args|
    credentials = OpenStruct.new(client: OpenStruct.new(updater_proc: Proc.new {}))
    storage = Google::Cloud::Storage::Project.new(Google::Cloud::Storage::Service.new("my-todo-project", credentials))

    storage.service.mocked_service = Minitest::Mock.new

    yield storage.service.mocked_service if block_given?

    storage
  end
end

def mock_logging
  Google::Cloud::Logging.stub_new do |*args|
    credentials = OpenStruct.new(client: OpenStruct.new(updater_proc: Proc.new {}))
    logging = Google::Cloud::Logging::Project.new(Google::Cloud::Logging::Service.new("my-project", credentials))

    logging.service.mocked_logging = Minitest::Mock.new
    logging.service.mocked_metrics = Minitest::Mock.new
    logging.service.mocked_sinks = Minitest::Mock.new

    if block_given?
      yield logging.service.mocked_logging,
            logging.service.mocked_metrics,
            logging.service.mocked_sinks
    end
    logging
  end
end

YARD::Doctest.configure do |doctest|
  ##
  # SKIP
  #

  # Skip all aliases, since tests would be exact duplicates
  doctest.skip "Google::Cloud::Logging::Logger#log"
  doctest.skip "Google::Cloud::Logging::Logger#sev_threshold="
  doctest.skip "Google::Cloud::Logging::Metric#refresh!"
  doctest.skip "Google::Cloud::Logging::Project#find_entries"
  doctest.skip "Google::Cloud::Logging::Project#new_entry"
  doctest.skip "Google::Cloud::Logging::Project#find_resource_descriptors"
  doctest.skip "Google::Cloud::Logging::Project#new_resource"
  doctest.skip "Google::Cloud::Logging::Project#find_sinks"
  doctest.skip "Google::Cloud::Logging::Project#new_sink"
  doctest.skip "Google::Cloud::Logging::Project#get_sink"
  doctest.skip "Google::Cloud::Logging::Project#find_sink"
  doctest.skip "Google::Cloud::Logging::Project#find_metrics"
  doctest.skip "Google::Cloud::Logging::Project#new_metric"
  doctest.skip "Google::Cloud::Logging::Project#get_metric"
  doctest.skip "Google::Cloud::Logging::Project#find_metric"
  doctest.skip "Google::Cloud::Logging::Project#find_logs"
  doctest.skip "Google::Cloud::Logging::Project#log_names"
  doctest.skip "Google::Cloud::Logging::Project#find_log_names"
  doctest.skip "Google::Cloud::Logging::Sink#start_time"
  doctest.skip "Google::Cloud::Logging::Sink#start_time="
  doctest.skip "Google::Cloud::Logging::Sink#end_time"
  doctest.skip "Google::Cloud::Logging::Sink#end_time="
  doctest.skip "Google::Cloud::Logging::Sink#refresh!"

  # Skip private methods
  doctest.skip "Google::Cloud::Logging::Middleware.default_monitored_resource"

  ##
  # BEFORE (mocking)
  #

  doctest.before "Google::Cloud.logging" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud#logging" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging.new" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.skip "Google::Cloud::Logging::Credentials" # occasionally getting "This code example is not yet mocked"

  doctest.before "Google::Cloud::Logging::Project" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#entry" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :write_log_entries, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#entries" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res("next_page_token"), [Hash]
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#write_entries" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :write_log_entries, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#entry" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :write_log_entries, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#logs" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_logs, list_logs_res("next_page_token"), [Hash]
      mock.expect :list_logs, list_logs_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#delete_log" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :delete_log, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#resource_descriptors" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_monitored_resource_descriptors, list_resource_descriptors_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#sink" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :get_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#sink@By default `nil` will be returned if the sink does not exist." do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :get_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#sinks" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :list_sinks, list_sinks_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#create_metric" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :create_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#metric" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :get_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#metric@By default `nil` will be returned if metric does not exist." do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :get_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#metrics" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :list_log_metrics, list_metrics_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Project#create_sink" do
    mock_storage do |mock|
      mock.expect :insert_bucket, bucket_gapi, ["my-todo-project", Google::Apis::StorageV1::Bucket, Hash]
      mock.expect :insert_bucket_access_control, object_access_control_gapi, ["my-bucket", Google::Apis::StorageV1::BucketAccessControl, Hash]
    end
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :create_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::ResourceDescriptor" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_monitored_resource_descriptors, list_resource_descriptors_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Sink" do
    mock_storage do |mock|
      mock.expect :insert_bucket, bucket_gapi, ["my-todo-project", Google::Apis::StorageV1::Bucket, Hash]
      mock.expect :insert_bucket_access_control, object_access_control_gapi, ["my-bucket", Google::Apis::StorageV1::BucketAccessControl, Hash]
    end
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :create_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Sink#delete" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :get_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Entry" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :write_log_entries, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Entry::List" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_log_entries, list_entries_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Log::List" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock.expect :list_logs, list_logs_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Metric" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :create_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Metric#save" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :get_log_metric, OpenStruct.new(name: "severe_errors"), [Hash]
      mock_metrics.expect :update_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Metric#delete" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :get_log_metric, OpenStruct.new(name: "severe_errors"), [Hash]
      mock_metrics.expect :delete_log_metric, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Metric#reload!" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :get_log_metric, OpenStruct.new(name: "severe_errors", filter: "logName:syslog"), [Hash]
      mock_metrics.expect :get_log_metric, OpenStruct.new(filter: "logName:syslog"), [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Metric::List" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_metrics.expect :list_log_metrics, list_metrics_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Sink#save" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :get_sink, OpenStruct.new(name: "severe_errors"), [Hash]
      mock_sinks.expect :update_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Sink#delete" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :get_sink, OpenStruct.new(name: "severe_errors"), [Hash]
      mock_sinks.expect :delete_sink, nil, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Sink::List" do
    mock_logging do |mock, mock_metrics, mock_sinks|
      mock_sinks.expect :list_sinks, list_sinks_res, [Hash]
    end
  end

  doctest.before "Google::Cloud::Logging::Middleware" do
    Google::Cloud.define_singleton_method :env do
      OpenStruct.new(:app_engine? => false, :container_engine? => false, :compute_engine? => false)
    end
  end

  doctest.before "Google::Cloud::Logging::Middleware.build_monitored_resource@If running from GAE, returns default resource" do
    Google::Cloud.define_singleton_method :env do
      OpenStruct.new(:app_engine? => true, :container_engine? => false, :compute_engine? => true)
    end
  end

  doctest.before "Google::Cloud::Logging::Middleware.build_monitored_resource@If running from GKE, returns default resource" do
    Google::Cloud.define_singleton_method :env do
      OpenStruct.new(:app_engine? => false, :container_engine? => true, :compute_engine? => true)
    end
  end

  doctest.before "Google::Cloud::Logging::Middleware.build_monitored_resource@If running from GCE, return default resource" do
    Google::Cloud.define_singleton_method :env do
      OpenStruct.new(:app_engine? => false, :container_engine? => false, :compute_engine? => true)
    end
  end
end

# Fixture helpers

def list_entries_res token = nil
  OpenStruct.new(
    response: Google::Cloud::Logging::V2::ListLogEntriesResponse.new(list_entries_hash(3, token))
  )
end

def list_logs_res token = nil
  OpenStruct.new(
    response: Google::Cloud::Logging::V2::ListLogsResponse.new(list_logs_hash(3, token))
  )
end

def list_resource_descriptors_res
  Google::Cloud::Logging::V2::ListMonitoredResourceDescriptorsResponse.new(list_resource_descriptors_hash(3))
end


def get_sink_res
  Google::Cloud::Logging::V2::LogSink.new(random_sink_hash.merge(name:  sink_name).to_hash)
end

def list_sinks_res
  OpenStruct.new(
    response: Google::Cloud::Logging::V2::ListSinksResponse.new(list_sinks_hash(3))
  )
end

def list_metrics_res
  OpenStruct.new(
    response: Google::Cloud::Logging::V2::ListLogMetricsResponse.new(list_metrics_hash(3))
  )
end

def list_resource_descriptors_hash count = 2, token = nil
  {
    resource_descriptors: count.times.map { random_resource_descriptor_hash },
    next_page_token: token
  }.delete_if { |_, v| v.nil? }
end

def list_metrics_hash count = 2, token = nil
  {
    metrics: count.times.map { random_metric_hash },
    next_page_token: token
  }.delete_if { |_, v| v.nil? }
end

def list_entries_hash count = 2, token = nil
  {
    entries: count.times.map { random_entry_hash },
    next_page_token: token
  }.delete_if { |_, v| v.nil? }
end

def random_entry_hash
  timestamp = Time.parse "2014-10-02T15:01:23.045123456Z"
  {
    log_name:   "projects/my-projectid/logs/syslog",
    resource:   random_resource_hash,
    timestamp:  {
      seconds:  timestamp.to_i,
      nanos:    timestamp.nsec
    },
    severity:   :DEFAULT,
    insert_id:  "abc123",
    labels:  {
      "env" =>  "production",
      "foo" =>  "bar"
    },
    text_payload:  "payload",
    http_request:  random_http_request_hash,
    operation:     random_operation_hash
  }
end

def random_http_request_hash
  {
    request_method:  "GET",
    request_url:  "http://test.local/foo?bar=baz",
    request_size:  123,
    status:  200,
    response_size:  456,
    user_agent:  "google-cloud/1.0.0",
    remote_ip:  "127.0.0.1",
    referer:  "http://test.local/referer",
    cache_hit:  false,
    cache_validated_with_origin_server:  false
  }
end

def random_operation_hash
  {
    id:  "xyz789",
    producer:  "MyApp.MyClass#my_method",
    first:  false,
    last:  false
  }
end

def random_resource_hash
  {
    type:  "gae_app",
    labels:  {
      "module_id"  => "1",
      "version_id" => "20150925t173233"
    }
  }
end

def random_resource_descriptor_hash
  {
    type:          "cloudsql_database",
    display_name:  "Cloud SQL Database",
    description:   "This resource is a Cloud SQL Database",
    labels:        [
      {
        key:           "database_id",
        description:   "The ID of the database."
      },
      {
        key:           "zone",
        value_type:    :STRING,
        description:   "The GCP zone in which the database is running."
      }
    ]
  }
end

def random_sink_hash
  timestamp = Time.parse "2014-10-02T15:01:23.045123456Z"
  {
    name:                   "my-severe-errors-to-pubsub",
    destination:            "storage.googleapis.com/a-bucket",
    filter:                 "logName:syslog AND severity>=ERROR",
    output_version_format:  :VERSION_FORMAT_UNSPECIFIED,
    writer_identity:        "roles/owner"
  }
end

def random_metric_hash
  {
    name:         "severe_errors",
    description:  "The servere errors metric",
    filter:       "logName:syslog AND severity>=ERROR"
  }
end

def list_sinks_hash count = 2, token = nil
  {
    sinks: count.times.map { random_sink_hash },
    next_page_token: token
  }.delete_if { |_, v| v.nil? }
end

def list_logs_hash count = 2, token = nil
  {
    log_names: count.times.map { "log-name" },
    next_page_token: token
  }.delete_if { |_, v| v.nil? }
end

# Storage helpers

def object_access_control_gapi
  entity = "project-owners-1234567890"
  Google::Apis::StorageV1::ObjectAccessControl.new entity: entity
end

def bucket_gapi name = "my-bucket"
  Google::Apis::StorageV1::Bucket.new(**random_bucket_hash(name))
end

def random_bucket_hash(name = "my-bucket",
  url_root="https://www.googleapis.com/storage/v1", location="US",
  storage_class="STANDARD", versioning=nil, logging_bucket=nil,
  logging_prefix=nil, website_main=nil, website_404=nil)
  versioning_config = { enabled:  versioning } if versioning
  { kind:  "storage#bucket",
    id:  name,
    selfLink:  "#{url_root}/b/#{name}",
    projectNumber:  "1234567890",
    name:  name,
    timeCreated:  Time.now,
    metageneration:  "1",
    owner:  { entity:  "project-owners-1234567890" },
    location:  location,
    cors:  [{origin: ["http://example.org"], method: ["GET","POST","DELETE"], response_header: ["X-My-Custom-Header"], max_age_seconds: 3600},{origin: ["http://example.org"], method: ["GET","POST","DELETE"], response_header: ["X-My-Custom-Header"], max_age_seconds: 3600}],
    logging:  logging_hash(logging_bucket, logging_prefix),
    storageClass:  storage_class,
    versioning:  versioning_config,
    website:  website_hash(website_main, website_404),
    etag:  "CAE=" }.delete_if { |_, v| v.nil? }
end

def logging_hash(bucket, prefix)
  { log_bucket:         bucket,
    log_object_prefix:  prefix,
  }.delete_if { |_, v| v.nil? } if bucket || prefix
end

def website_hash(website_main, website_404)
  { main_page_suffix:  website_main,
    not_found_page:    website_404,
  }.delete_if { |_, v| v.nil? } if website_main || website_404
end
