package mocktracer // import "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"

import (
	"fmt"
	"sync"
	"time"

	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
)

var _ ddtrace.Span = (*mockspan)(nil)
var _ Span = (*mockspan)(nil)

// Span is an interface that allows querying a span returned by the mock tracer.
type Span interface {
	// SpanID returns the span's ID.
	SpanID() uint64

	// TraceID returns the span's trace ID.
	TraceID() uint64

	// ParentID returns the span's parent ID.
	ParentID() uint64

	// StartTime returns the time when the span has started.
	StartTime() time.Time

	// FinishTime returns the time when the span has finished.
	FinishTime() time.Time

	// OperationName returns the operation name held by this span.
	OperationName() string

	// Tag returns the value of the tag at key k.
	Tag(k string) interface{}

	// Tags returns a copy of all the tags in this span.
	Tags() map[string]interface{}

	// Context returns the span's SpanContext.
	Context() ddtrace.SpanContext

	// Stringer allows pretty-printing the span's fields for debugging.
	fmt.Stringer
}

func newSpan(t *mocktracer, operationName string, cfg *ddtrace.StartSpanConfig) *mockspan {
	if cfg.Tags == nil {
		cfg.Tags = make(map[string]interface{})
	}
	if cfg.Tags[ext.ResourceName] == nil {
		cfg.Tags[ext.ResourceName] = operationName
	}
	s := &mockspan{
		name:   operationName,
		tracer: t,
	}
	if cfg.StartTime.IsZero() {
		s.startTime = time.Now()
	} else {
		s.startTime = cfg.StartTime
	}
	id := nextID()
	s.context = &spanContext{spanID: id, traceID: id, span: s}
	if ctx, ok := cfg.Parent.(*spanContext); ok {
		if ctx.span != nil && s.tags[ext.ServiceName] == nil {
			// if we have a local parent and no service, inherit the parent's
			s.SetTag(ext.ServiceName, ctx.span.Tag(ext.ServiceName))
		}
		if ctx.hasSamplingPriority() {
			s.SetTag(ext.SamplingPriority, ctx.samplingPriority())
		}
		s.parentID = ctx.spanID
		s.context.priority = ctx.samplingPriority()
		s.context.hasPriority = ctx.hasSamplingPriority()
		s.context.traceID = ctx.traceID
		s.context.baggage = make(map[string]string, len(ctx.baggage))
		ctx.ForeachBaggageItem(func(k, v string) bool {
			s.context.baggage[k] = v
			return true
		})
	}
	for k, v := range cfg.Tags {
		s.SetTag(k, v)
	}
	return s
}

type mockspan struct {
	sync.RWMutex // guards below fields
	name         string
	tags         map[string]interface{}
	finishTime   time.Time

	startTime time.Time
	parentID  uint64
	context   *spanContext
	tracer    *mocktracer
}

// SetTag sets a given tag on the span.
func (s *mockspan) SetTag(key string, value interface{}) {
	s.Lock()
	defer s.Unlock()
	if s.tags == nil {
		s.tags = make(map[string]interface{}, 1)
	}
	if key == ext.SamplingPriority {
		switch p := value.(type) {
		case int:
			s.context.setSamplingPriority(p)
		case float64:
			s.context.setSamplingPriority(int(p))
		}
	}
	s.tags[key] = value
}

func (s *mockspan) FinishTime() time.Time {
	s.RLock()
	defer s.RUnlock()
	return s.finishTime
}

func (s *mockspan) StartTime() time.Time { return s.startTime }

func (s *mockspan) Tag(k string) interface{} {
	s.RLock()
	defer s.RUnlock()
	return s.tags[k]
}

func (s *mockspan) Tags() map[string]interface{} {
	s.RLock()
	defer s.RUnlock()
	// copy
	cp := make(map[string]interface{}, len(s.tags))
	for k, v := range s.tags {
		cp[k] = v
	}
	return cp
}

func (s *mockspan) TraceID() uint64 { return s.context.traceID }

func (s *mockspan) SpanID() uint64 { return s.context.spanID }

func (s *mockspan) ParentID() uint64 { return s.parentID }

func (s *mockspan) OperationName() string {
	s.RLock()
	defer s.RUnlock()
	return s.name
}

// SetOperationName resets the original operation name to the given one.
func (s *mockspan) SetOperationName(operationName string) {
	s.Lock()
	defer s.Unlock()
	s.name = operationName
	return
}

// BaggageItem returns the baggage item with the given key.
func (s *mockspan) BaggageItem(key string) string {
	return s.context.baggageItem(key)
}

// SetBaggageItem sets a new baggage item at the given key. The baggage
// item should propagate to all descendant spans, both in- and cross-process.
func (s *mockspan) SetBaggageItem(key, val string) {
	s.context.setBaggageItem(key, val)
	return
}

// Finish finishes the current span with the given options.
func (s *mockspan) Finish(opts ...ddtrace.FinishOption) {
	var cfg ddtrace.FinishConfig
	for _, fn := range opts {
		fn(&cfg)
	}
	var t time.Time
	if cfg.FinishTime.IsZero() {
		t = time.Now()
	} else {
		t = cfg.FinishTime
	}
	if cfg.Error != nil {
		s.SetTag(ext.Error, cfg.Error)
	}
	s.Lock()
	s.finishTime = t
	s.Unlock()
	s.tracer.addFinishedSpan(s)
}

// String implements fmt.Stringer.
func (s *mockspan) String() string {
	sc := s.context
	return fmt.Sprintf(`
name: %s
tags: %#v
start: %s
finish: %s
id: %d
parent: %d
trace: %d
baggage: %#v
`, s.name, s.tags, s.startTime, s.finishTime, sc.spanID, s.parentID, sc.traceID, sc.baggage)
}

// Context returns the SpanContext of this Span.
func (s *mockspan) Context() ddtrace.SpanContext { return s.context }
