# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

require 'test_helper'

describe OpenTelemetry::SDK::Trace::Tracer do
  Tracer = OpenTelemetry::SDK::Trace::Tracer
  SpanLimits = OpenTelemetry::SDK::Trace::SpanLimits

  let(:tracer_provider) { OpenTelemetry::SDK::Trace::TracerProvider.new }
  let(:tracer) do
    OpenTelemetry.tracer_provider = tracer_provider
    OpenTelemetry.tracer_provider.tracer('component-tracer', '1.0.0')
  end
  let(:record_sampler) do
    Samplers::ConstantSampler.new(decision: Decision::RECORD_ONLY, description: 'RecordSampler')
  end

  describe '#start_root_span' do
    it 'provides a default name' do
      _(tracer.start_root_span(nil).name).wont_be_nil
    end

    it 'defaults to span kind internal when not specified' do
      _(tracer.start_root_span(nil).kind).must_equal(:internal)
    end

    it 'returns a valid span' do
      span = tracer.start_root_span('root')
      _(span.context).must_be :valid?
    end

    it 'creates a span with IDs generated by the configured id_generator' do
      span_id = 'I am sID'
      trace_id = 'I am a trace ID '
      mock_id_generator = Minitest::Mock.new
      mock_id_generator.expect(:generate_span_id, span_id)
      mock_id_generator.expect(:generate_trace_id, trace_id)
      tracer_provider.id_generator = mock_id_generator
      span = tracer.start_root_span('span')
      _(span.context.trace_id).must_equal trace_id
      _(span.context.span_id).must_equal span_id
    end

    it 'returns a no-op span if sampler says do not record events' do
      tracer_provider.sampler = Samplers::ALWAYS_OFF
      span = tracer.start_root_span('root')
      _(span.context.trace_flags).wont_be :sampled?
      _(span).wont_be :recording?
    end

    it 'returns an unsampled span if sampler says record, but do not sample' do
      tracer_provider.sampler = record_sampler
      span = tracer.start_root_span('root')
      _(span.context.trace_flags).wont_be :sampled?
      _(span).must_be :recording?
    end

    it 'returns a sampled span if sampler says sample' do
      tracer_provider.sampler = Samplers::ALWAYS_ON
      span = tracer.start_root_span('root')
      _(span.context.trace_flags).must_be :sampled?
      _(span).must_be :recording?
    end

    it 'calls the sampler with all parameters except parent_context' do
      links = Minitest::Mock.new
      name = 'span'
      trace_id = OpenTelemetry::Trace.generate_trace_id
      kind = Minitest::Mock.new
      attributes = Minitest::Mock.new
      result = Result.new(decision: Decision::DROP, tracestate: nil)
      mock_sampler = Minitest::Mock.new
      mock_sampler.expect(:should_sample?, result) do |args|
        args[:trace_id] == trace_id &&
          args[:links] === links && # rubocop:disable Style/CaseEquality
          args[:name] === name && # rubocop:disable Style/CaseEquality
          args[:kind] === kind && # rubocop:disable Style/CaseEquality
          args[:attributes] === attributes && # rubocop:disable Style/CaseEquality
          !args[:parent_context].nil? &&
          !OpenTelemetry::Trace.current_span(args[:parent_context]).context.valid?
      end
      tracer_provider.sampler = mock_sampler
      OpenTelemetry::Trace.stub :generate_trace_id, trace_id do
        tracer.start_root_span(name, attributes: attributes, links: links, kind: kind)
      end
      mock_sampler.verify
    end

    it 'returns a no-op span if tracer has shutdown' do
      tracer_provider.shutdown
      span = tracer.start_root_span('root')
      _(span.context.trace_flags).wont_be :sampled?
      _(span).wont_be :recording?
    end

    it 'creates a span with all supplied parameters' do
      context = OpenTelemetry::Trace::SpanContext.new
      links = [OpenTelemetry::Trace::Link.new(context)]
      name = 'span'
      kind = OpenTelemetry::Trace::SpanKind::INTERNAL
      attributes = { '1' => 1 }
      start_timestamp = Time.now
      span = tracer.start_root_span(name, attributes: attributes, links: links, kind: kind, start_timestamp: start_timestamp)
      _(span.links).must_equal(links)
      _(span.name).must_equal(name)
      _(span.kind).must_equal(kind)
      _(span.attributes).must_equal(attributes)
      _(span.start_timestamp).must_equal(OpenTelemetry::TestHelpers.exportable_timestamp(start_timestamp))
    end

    it 'creates a span with sampler attributes added after supplied attributes' do
      sampler_attributes = { '1' => 1 }
      mock_sampler = Minitest::Mock.new
      result = Result.new(decision: Decision::RECORD_ONLY, attributes: sampler_attributes, tracestate: nil)
      mock_sampler.expect(:should_sample?, result, [], trace_id: String, parent_context: OpenTelemetry::Context, links: nil, name: String, kind: Symbol, attributes: Hash)
      tracer_provider.sampler = mock_sampler
      span = tracer.start_root_span('op', attributes: { '1' => 0, '2' => 2 })
      _(span.attributes).must_equal('1' => 1, '2' => 2)
    end

    it 'ignores the implicit current span context' do
      root = nil
      span = nil
      tracer.in_span('root') do |s|
        root = s
        span = tracer.start_root_span('also root')
      end
      _(span.parent_span_id).must_equal(OpenTelemetry::Trace::INVALID_SPAN_ID)
      _(span.context.trace_id).wont_equal(root.context.trace_id)
    end

    it 'trims link attributes' do
      tracer_provider.span_limits = SpanLimits.new(link_attribute_count_limit: 1)
      link = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '1' => 1, '2' => 2)
      span = tracer.start_root_span('root', links: [link])
      _(span.links.first.attributes.size).must_equal(1)
    end

    it 'trims links' do
      tracer_provider.span_limits = SpanLimits.new(link_count_limit: 1)
      link1 = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '1' => 1)
      link2 = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '2' => 2)
      span = tracer.start_root_span('root', links: [link1, link2])
      _(span.links.size).must_equal(1)
      _(span.links.first).must_equal(link1)
    end
  end

  describe '#start_span' do
    let(:span_context) do
      OpenTelemetry::Trace::SpanContext.new(tracestate: 'vendorname=opaquevalue', trace_flags: OpenTelemetry::Trace::TraceFlags::SAMPLED)
    end
    let(:context) do
      OpenTelemetry::Trace.context_with_span(
        OpenTelemetry::Trace.non_recording_span(span_context),
        parent_context: OpenTelemetry::Context.empty
      )
    end

    it 'provides a default name' do
      _(tracer.start_span(nil, with_parent: context).name).wont_be_nil
    end

    it 'returns a valid span' do
      span = tracer.start_span('op', with_parent: context)
      _(span.context).must_be :valid?
    end

    it 'returns a span with the same trace ID as the parent context' do
      span = tracer.start_span('op', with_parent: context)
      _(span.context.trace_id).must_equal(span_context.trace_id)
    end

    it 'returns a span with the parent context span ID' do
      span = tracer.start_span('op', with_parent: context)
      _(span.parent_span_id).must_equal(span_context.span_id)
    end

    it 'returns a span with the parent context tracestate' do
      span = tracer.start_span('op', with_parent: context)
      _(span.context.tracestate).must_equal(span_context.tracestate)
    end

    it 'returns a no-op span if sampler says do not record events' do
      tracer_provider.sampler = Samplers::ALWAYS_OFF
      span = tracer.start_span('op', with_parent: context)
      _(span.context.trace_flags).wont_be :sampled?
      _(span).wont_be :recording?
    end

    it 'returns a no-op span within an untraced block' do
      tracer_provider.sampler = Samplers::ALWAYS_ON
      span = OpenTelemetry::Common::Utilities.untraced { tracer.start_span('op') }
      _(span.context.trace_flags).wont_be :sampled?
      _(span).wont_be :recording?
    end

    it 'returns an unsampled span if sampler says record, but do not sample' do
      tracer_provider.sampler = record_sampler
      span = tracer.start_span('op', with_parent: context)
      _(span.context.trace_flags).wont_be :sampled?
      _(span).must_be :recording?
    end

    it 'returns a sampled span if sampler says sample' do
      tracer_provider.sampler = Samplers::ALWAYS_ON
      span = tracer.start_span('op', with_parent: context)
      _(span.context.trace_flags).must_be :sampled?
      _(span).must_be :recording?
    end

    it 'calls the sampler with all parameters' do
      links = Minitest::Mock.new
      name = 'span'
      kind = Minitest::Mock.new
      attributes = Minitest::Mock.new
      result = Result.new(decision: Decision::DROP, tracestate: nil)
      mock_sampler = Minitest::Mock.new
      mock_sampler.expect(:should_sample?, result, [], trace_id: span_context.trace_id, parent_context: context, links: links, name: name, kind: kind, attributes: attributes)
      tracer_provider.sampler = mock_sampler
      tracer.start_span(name, with_parent: context, attributes: attributes, links: links, kind: kind)
      mock_sampler.verify
    end

    it 'returns a no-op span with parent trace ID if tracer has shutdown' do
      tracer_provider.shutdown
      span = tracer.start_span('op', with_parent: context)
      _(span.context.trace_flags).wont_be :sampled?
      _(span).wont_be :recording?
      _(span.context.trace_id).must_equal(span_context.trace_id)
    end

    it 'creates a span with IDs generated by the configured id_generator' do
      span_id = 'I am sID'
      mock_id_generator = Minitest::Mock.new
      mock_id_generator.expect(:generate_span_id, span_id)
      tracer_provider.id_generator = mock_id_generator
      span = tracer.start_span('span', with_parent: context)
      _(span.context.trace_id).must_equal(span_context.trace_id)
      _(span.context.span_id).must_equal span_id
    end

    it 'creates a span with the provided instrumentation scope' do
      span = tracer.start_span('span', with_parent: context)
      _(span.instrumentation_scope.name).must_equal('component-tracer')
      _(span.instrumentation_scope.version).must_equal('1.0.0')
    end

    it 'creates a span with all supplied parameters' do
      links = [OpenTelemetry::Trace::Link.new(span_context)]
      name = 'span'
      kind = OpenTelemetry::Trace::SpanKind::INTERNAL
      attributes = { '1' => 1 }
      start_timestamp = Time.now
      span = tracer.start_span(name, with_parent: context, attributes: attributes, links: links, kind: kind, start_timestamp: start_timestamp)
      _(span.links).must_equal(links)
      _(span.name).must_equal(name)
      _(span.kind).must_equal(kind)
      _(span.attributes).must_equal(attributes)
      _(span.parent_span_id).must_equal(span_context.span_id)
      _(span.context.trace_id).must_equal(span_context.trace_id)
      _(span.start_timestamp).must_equal(OpenTelemetry::TestHelpers.exportable_timestamp(start_timestamp))
    end

    it 'creates a span with sampler attributes added after supplied attributes' do
      sampler_attributes = { '1' => 1 }
      mock_sampler = Minitest::Mock.new
      result = Result.new(decision: Decision::RECORD_ONLY, attributes: sampler_attributes, tracestate: nil)
      mock_sampler.expect(:should_sample?, result, [], trace_id: String, parent_context: OpenTelemetry::Context, links: nil, name: String, kind: Symbol, attributes: Hash)
      tracer_provider.sampler = mock_sampler
      span = tracer.start_span('op', with_parent: context, attributes: { '1' => 0, '2' => 2 })
      _(span.attributes).must_equal('1' => 1, '2' => 2)
    end

    it 'uses the context from parent if supplied' do
      parent = tracer.start_root_span('root')
      parent_ctx = OpenTelemetry::Trace.context_with_span(parent)
      span = tracer.start_span('child', with_parent: parent_ctx)
      _(span.parent_span_id).must_equal(parent.context.span_id)
      _(span.context.trace_id).must_equal(parent.context.trace_id)
    end

    it 'uses the implicit current span context by default' do
      root = nil
      span = nil
      tracer.in_span('root') do |s|
        root = s
        span = tracer.start_span('child')
      end
      _(span.parent_span_id).must_equal(root.context.span_id)
      _(span.context.trace_id).must_equal(root.context.trace_id)
    end

    it 'trims link attributes' do
      tracer_provider.span_limits = SpanLimits.new(link_attribute_count_limit: 1)
      link = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '1' => 1, '2' => 2)
      span = tracer.start_span('op', with_parent: context, links: [link])
      _(span.links.first.attributes.size).must_equal(1)
    end

    it 'trims links' do
      tracer_provider.span_limits = SpanLimits.new(link_count_limit: 1)
      link1 = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '1' => 1)
      link2 = OpenTelemetry::Trace::Link.new(OpenTelemetry::Trace::SpanContext.new, '2' => 2)
      span = tracer.start_span('op', with_parent: context, links: [link1, link2])
      _(span.links.size).must_equal(1)
      _(span.links.first).must_equal(link1)
    end
  end

  describe '#in_span' do
    it 'records and reraises exceptions' do
      span = nil
      _(proc do
        tracer.in_span('op') do |s|
          span = s
          raise 'this is fine'
        end
      end).must_raise(RuntimeError)

      _(span.events.size).must_equal(1)

      _(span.events[0].name).must_equal('exception')
      _(span.events[0].attributes['exception.message']).must_equal('this is fine')
      _(span.status.code).must_equal(OpenTelemetry::Trace::Status::ERROR)
      _(span.status.description).must_equal('Unhandled exception of type: RuntimeError')
    end

    it 'does not record an exception if record_exception is false' do
      span = nil
      _(proc do
        tracer.in_span('op', record_exception: false) do |s|
          span = s
          raise 'this is fine'
        end
      end).must_raise(RuntimeError)

      _(span.events).must_be_nil
    end

    it 'yields a no-op span within an untraced block' do
      tracer.in_span('root') do
        span_id = OpenTelemetry::Trace.current_span.context.span_id
        OpenTelemetry::Common::Utilities.untraced do
          tracer.in_span('op') do |span|
            _(span).must_be_instance_of(OpenTelemetry::Trace::Span)
            _(span.context.span_id).must_equal span_id
            _(span.context.trace_flags).wont_be :sampled?
            _(span).wont_be :recording?
          end
        end
      end
    end

    it 'does not log "Calling finish on an ended Span" warnings when untraced? is called' do
      OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
        tracer.in_span('root') do
          OpenTelemetry::Common::Utilities.untraced { tracer.in_span('op') {} }
        end
        _(log_stream.string).wont_include 'Calling finish on an ended Span'
      end
    end
  end
end
