# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you 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.

# frozen_string_literal: true

require 'spec_helper'

module ElasticAPM
  module Transport
    module Serializers
      RSpec.describe SpanSerializer do
        let(:config) { Config.new }
        subject { described_class.new config }

        describe '#build', :mock_time do
          let(:transaction) { Transaction.new(config: config).start }

          let(:trace_context) do
            traceparent =
              TraceContext::Traceparent.parse("00-#{'1' * 32}-#{'2' * 16}-01")
            TraceContext.new(traceparent: traceparent)
          end

          let :span do
            Span.new(
              name: 'Span',
              transaction: transaction,
              parent: transaction,
              trace_context: trace_context,
              sync: true
            ).tap do |span|
              span.start
              travel 10_000
              span.stop
            end
          end

          let(:result) { subject.build(span) }

          it 'builds' do
            expect(result).to match(
              span: {
                id: /.{16}/,
                transaction_id: transaction.id,
                parent_id: span.parent_id,
                trace_id: span.trace_id,
                name: 'Span',
                type: 'custom',
                context: { sync: true },
                stacktrace: [],
                sample_rate: 1,
                timestamp: 694_224_000_000_000,
                duration: 10,
                outcome: nil
              }
            )
          end

          context 'with a context' do
            let(:span) do
              Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                context: Span::Context.new(
                  db: { statement: 'asd' },
                  http: { url: 'dsa' },
                  sync: false,
                  labels: { foo: 'bar' },
                  service: {target: {name: 'test', type: 'db'}},
                  links: [Span::Context::Links::Link.new(trace_id: 'abc', span_id: '123')]
                )
              )
            end

            it 'adds context object' do
              expect(result.dig(:span, :context, :db, :statement))
                .to eq 'asd'
              expect(result.dig(:span, :context, :http, :url)).to eq 'dsa'
              expect(result.dig(:span, :context, :sync)).to eq false
              expect(result.dig(:span, :context, :tags, :foo)).to eq 'bar'
              expect(result.dig(:span, :context, :service, :target, :name)).to eq 'test'
              expect(result.dig(:span, :context, :service, :target, :type)).to eq 'db'
              expect(result.dig(:span, :context, :links).length).to eq 1
            end

            context 'with rows_affected' do
              let(:span) do
                Span.new(
                  name: 'Span',
                  transaction: transaction,
                  parent: transaction,
                  trace_context: trace_context,
                  context: Span::Context.new(
                    db: { rows_affected: 2 }
                  )
                )
              end

              it 'adds rows_affected' do
                expect(result.dig(:span, :context, :db, :rows_affected))
                  .to eq 2
              end
            end

            context 'when sync is nil' do
              let(:span) do
                Span.new(
                  name: 'Span',
                  transaction: transaction,
                  parent: transaction,
                  trace_context: trace_context,
                  context: Span::Context.new(
                    db: { statement: 'asd' },
                    http: { url: 'dsa' }
                  )
                )
              end

              it 'adds context object' do
                expect(result.dig(:span, :context, :db, :statement))
                  .to eq 'asd'
                expect(result.dig(:span, :context, :http, :url)).to eq 'dsa'
                expect(result[:span][:context].key?(:sync)).to be false
              end
            end
          end

          context 'with a destination' do
            it 'adds destination object' do
              span = Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                context: Span::Context.new(
                  destination: {
                    service: {
                      name: 'a',
                      resource: 'b',
                      type: 'c',
                    },
                    address: 'd',
                    port: 8080
                  }
                )
              )

              result = subject.build(span)

              expect(result.dig(:span, :context, :destination)).to match(
                {
                  service: {
                    name: 'a',
                    resource: 'b',
                    type: 'c'
                  },
                  address: 'd',
                  port: 8080
                }
              )
            end
          end

          context 'with a message' do
            it 'adds message object' do
              span = Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                context: Span::Context.new(
                  message: {
                    queue_name: 'my_queue',
                    age_ms: 1000
                  }
                )
              )

              result = subject.build(span)

              expect(result.dig(:span, :context, :message)).to match(
                {
                  queue: {
                    name: 'my_queue'
                  },
                  age: {
                    ms: 1000
                  }
                 }
               )
            end
          end

          context 'with a large db.statement' do
            it 'truncates to 10k chars' do
              span = Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                context: Span::Context.new(
                  db: { statement: 'X' * 11_000 }
                )
              )

              result = subject.build(span)

              statement = result.dig(:span, :context, :db, :statement)
              expect(statement.length).to be(10_000)
            end
          end

          context 'with split types' do
            let(:span) do
              Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                type: 'a',
                subtype: 'b',
                action: 'c'
              )
            end

            it 'joins them for sending' do
              expect(result[:span][:type]).to eq 'a.b.c'
            end
          end

          context 'with outcome' do
            it 'adds the outcome' do
              span = Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context
              )

              span.outcome = 'success'
              result = subject.build(span)
              expect(result[:span][:outcome]).to eq 'success'
            end
          end

          context 'with a destination and cloud' do
            it 'adds destination with cloud' do
              span = Span.new(
                name: 'Span',
                transaction: transaction,
                parent: transaction,
                trace_context: trace_context,
                context: Span::Context.new(
                  destination: {
                    service: { resource: 'a' },
                    cloud: { region: 'b' }
                  }
                )
              )

              # set auto-infered destination.service fields
              span.start.done

              result = subject.build(span)

              expect(result.dig(:span, :context, :destination, :service))
                .to match({ resource: 'a', name: '', type: '' })

              expect(result.dig(:span, :context, :destination, :cloud))
                .to match({ region: 'b' })
            end
          end
        end
      end
    end
  end
end
