// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package testtrace

import (
	
	
	
	
	
)

// Validator is a type used for validating a stream of trace.Events.
type Validator struct {
	lastTs   trace.Time
	gs       map[trace.GoID]*goState
	ps       map[trace.ProcID]*procState
	ms       map[trace.ThreadID]*schedContext
	ranges   map[trace.ResourceID][]string
	tasks    map[trace.TaskID]string
	seenSync bool
	Go121    bool
}

type schedContext struct {
	M trace.ThreadID
	P trace.ProcID
	G trace.GoID
}

type goState struct {
	state   trace.GoState
	binding *schedContext
}

type procState struct {
	state   trace.ProcState
	binding *schedContext
}

// NewValidator creates a new Validator.
func () *Validator {
	return &Validator{
		gs:     make(map[trace.GoID]*goState),
		ps:     make(map[trace.ProcID]*procState),
		ms:     make(map[trace.ThreadID]*schedContext),
		ranges: make(map[trace.ResourceID][]string),
		tasks:  make(map[trace.TaskID]string),
	}
}

// Event validates ev as the next event in a stream of trace.Events.
//
// Returns an error if validation fails.
func ( *Validator) ( trace.Event) error {
	 := new(errAccumulator)

	// Validate timestamp order.
	if .lastTs != 0 {
		if .Time() <= .lastTs {
			.Errorf("timestamp out-of-order for %+v", )
		} else {
			.lastTs = .Time()
		}
	} else {
		.lastTs = .Time()
	}

	// Validate event stack.
	checkStack(, .Stack())

	switch .Kind() {
	case trace.EventSync:
		// Just record that we've seen a Sync at some point.
		.seenSync = true
	case trace.EventMetric:
		 := .Metric()
		if !strings.Contains(.Name, ":") {
			// Should have a ":" as per runtime/metrics convention.
			.Errorf("invalid metric name %q", .Name)
		}
		// Make sure the value is OK.
		if .Value.Kind() == trace.ValueBad {
			.Errorf("invalid value")
		}
		switch .Value.Kind() {
		case trace.ValueUint64:
			// Just make sure it doesn't panic.
			_ = .Value.Uint64()
		}
	case trace.EventLabel:
		 := .Label()

		// Check label.
		if .Label == "" {
			.Errorf("invalid label %q", .Label)
		}

		// Check label resource.
		if .Resource.Kind == trace.ResourceNone {
			.Errorf("label resource none")
		}
		switch .Resource.Kind {
		case trace.ResourceGoroutine:
			 := .Resource.Goroutine()
			if ,  := .gs[]; ! {
				.Errorf("label for invalid goroutine %d", )
			}
		case trace.ResourceProc:
			 := .Resource.Proc()
			if ,  := .ps[]; ! {
				.Errorf("label for invalid proc %d", )
			}
		case trace.ResourceThread:
			 := .Resource.Thread()
			if ,  := .ms[]; ! {
				.Errorf("label for invalid thread %d", )
			}
		}
	case trace.EventStackSample:
		// Not much to check here. It's basically a sched context and a stack.
		// The sched context is also not guaranteed to align with other events.
		// We already checked the stack above.
	case trace.EventStateTransition:
		// Validate state transitions.
		//
		// TODO(mknyszek): A lot of logic is duplicated between goroutines and procs.
		// The two are intentionally handled identically; from the perspective of the
		// API, resources all have the same general properties. Consider making this
		// code generic over resources and implementing validation just once.
		 := .StateTransition()
		checkStack(, .Stack)
		switch .Resource.Kind {
		case trace.ResourceGoroutine:
			// Basic state transition validation.
			 := .Resource.Goroutine()
			,  := .Goroutine()
			if  == trace.GoUndetermined {
				.Errorf("transition to undetermined state for goroutine %d", )
			}
			if .seenSync &&  == trace.GoUndetermined {
				.Errorf("undetermined goroutine %d after first global sync", )
			}
			if  == trace.GoNotExist && .hasAnyRange(trace.MakeResourceID()) {
				.Errorf("goroutine %d died with active ranges", )
			}
			,  := .gs[]
			if  {
				if  != .state {
					.Errorf("bad old state for goroutine %d: got %s, want %s", , , .state)
				}
				.state = 
			} else {
				if  != trace.GoUndetermined &&  != trace.GoNotExist {
					.Errorf("bad old state for unregistered goroutine %d: %s", , )
				}
				 = &goState{state: }
				.gs[] = 
			}
			// Validate sched context.
			if .Executing() {
				 := .getOrCreateThread(, , .Thread())
				if  != nil {
					if .G != trace.NoGoroutine && .G !=  {
						.Errorf("tried to run goroutine %d when one was already executing (%d) on thread %d", , .G, .Thread())
					}
					.G = 
					.binding = 
				}
			} else if .Executing() && !.Executing() {
				if .Stack != .Stack() {
					// This is a case where the transition is happening to a goroutine that is also executing, so
					// these two stacks should always match.
					.Errorf("StateTransition.Stack doesn't match Event.Stack")
				}
				 := .binding
				if  != nil {
					if .G !=  {
						.Errorf("tried to stop goroutine %d when it wasn't currently executing (currently executing %d) on thread %d", , .G, .Thread())
					}
					.G = trace.NoGoroutine
					.binding = nil
				} else {
					.Errorf("stopping goroutine %d not bound to any active context", )
				}
			}
		case trace.ResourceProc:
			// Basic state transition validation.
			 := .Resource.Proc()
			,  := .Proc()
			if  == trace.ProcUndetermined {
				.Errorf("transition to undetermined state for proc %d", )
			}
			if .seenSync &&  == trace.ProcUndetermined {
				.Errorf("undetermined proc %d after first global sync", )
			}
			if  == trace.ProcNotExist && .hasAnyRange(trace.MakeResourceID()) {
				.Errorf("proc %d died with active ranges", )
			}
			,  := .ps[]
			if  {
				if  != .state {
					.Errorf("bad old state for proc %d: got %s, want %s", , , .state)
				}
				.state = 
			} else {
				if  != trace.ProcUndetermined &&  != trace.ProcNotExist {
					.Errorf("bad old state for unregistered proc %d: %s", , )
				}
				 = &procState{state: }
				.ps[] = 
			}
			// Validate sched context.
			if .Executing() {
				 := .getOrCreateThread(, , .Thread())
				if  != nil {
					if .P != trace.NoProc && .P !=  {
						.Errorf("tried to run proc %d when one was already executing (%d) on thread %d", , .P, .Thread())
					}
					.P = 
					.binding = 
				}
			} else if .Executing() && !.Executing() {
				 := .binding
				if  != nil {
					if .P !=  {
						.Errorf("tried to stop proc %d when it wasn't currently executing (currently executing %d) on thread %d", , .P, .M)
					}
					.P = trace.NoProc
					.binding = nil
				} else {
					.Errorf("stopping proc %d not bound to any active context", )
				}
			}
		}
	case trace.EventRangeBegin, trace.EventRangeActive, trace.EventRangeEnd:
		// Validate ranges.
		 := .Range()
		switch .Kind() {
		case trace.EventRangeBegin:
			if .hasRange(.Scope, .Name) {
				.Errorf("already active range %q on %v begun again", .Name, .Scope)
			}
			.addRange(.Scope, .Name)
		case trace.EventRangeActive:
			if !.hasRange(.Scope, .Name) {
				.addRange(.Scope, .Name)
			}
		case trace.EventRangeEnd:
			if !.hasRange(.Scope, .Name) {
				.Errorf("inactive range %q on %v ended", .Name, .Scope)
			}
			.deleteRange(.Scope, .Name)
		}
	case trace.EventTaskBegin:
		// Validate task begin.
		 := .Task()
		if .ID == trace.NoTask || .ID == trace.BackgroundTask {
			// The background task should never have an event emitted for it.
			.Errorf("found invalid task ID for task of type %s", .Type)
		}
		if .Parent == trace.BackgroundTask {
			// It's not possible for a task to be a subtask of the background task.
			.Errorf("found background task as the parent for task of type %s", .Type)
		}
		// N.B. Don't check the task type. Empty string is a valid task type.
		.tasks[.ID] = .Type
	case trace.EventTaskEnd:
		// Validate task end.
		// We can see a task end without a begin, so ignore a task without information.
		// Instead, if we've seen the task begin, just make sure the task end lines up.
		 := .Task()
		if ,  := .tasks[.ID];  {
			if .Type !=  {
				.Errorf("task end type %q doesn't match task start type %q for task %d", .Type, , .ID)
			}
			delete(.tasks, .ID)
		}
	case trace.EventLog:
		// There's really not much here to check, except that we can
		// generate a Log. The category and message are entirely user-created,
		// so we can't make any assumptions as to what they are. We also
		// can't validate the task, because proving the task's existence is very
		// much best-effort.
		_ = .Log()
	}
	return .Errors()
}

func ( *Validator) ( trace.ResourceID,  string) bool {
	,  := .ranges[]
	return  && slices.Contains(, )
}

func ( *Validator) ( trace.ResourceID,  string) {
	,  := .ranges[]
	 = append(, )
	.ranges[] = 
}

func ( *Validator) ( trace.ResourceID) bool {
	,  := .ranges[]
	return  && len() != 0
}

func ( *Validator) ( trace.ResourceID,  string) {
	,  := .ranges[]
	if ! {
		return
	}
	 := slices.Index(, )
	if  < 0 {
		return
	}
	.ranges[] = slices.Delete(, , +1)
}

func ( *Validator) ( *errAccumulator,  trace.Event,  trace.ThreadID) *schedContext {
	 := func() bool {
		// Be lenient about GoUndetermined -> GoSyscall transitions if they
		// originate from an old trace. These transitions lack thread
		// information in trace formats older than 1.22.
		if !.Go121 {
			return false
		}
		if .Kind() != trace.EventStateTransition {
			return false
		}
		 := .StateTransition()
		if .Resource.Kind != trace.ResourceGoroutine {
			return false
		}
		,  := .Goroutine()
		return  == trace.GoUndetermined &&  == trace.GoSyscall
	}
	if  == trace.NoThread && !() {
		.Errorf("must have thread, but thread ID is none")
		return nil
	}
	,  := .ms[]
	if ! {
		 = &schedContext{M: , P: trace.NoProc, G: trace.NoGoroutine}
		.ms[] = 
		return 
	}
	return 
}

func checkStack( *errAccumulator,  trace.Stack) {
	// Check for non-empty values, but we also check for crashes due to incorrect validation.
	 := 0
	.Frames(func( trace.StackFrame) bool {
		if  == 0 {
			// Allow for one fully zero stack.
			//
			// TODO(mknyszek): Investigate why that happens.
			return true
		}
		if .Func == "" || .File == "" || .PC == 0 || .Line == 0 {
			.Errorf("invalid stack frame %#v: missing information", )
		}
		++
		return true
	})
}

type errAccumulator struct {
	errs []error
}

func ( *errAccumulator) ( string,  ...any) {
	.errs = append(.errs, fmt.Errorf(, ...))
}

func ( *errAccumulator) () error {
	return errors.Join(.errs...)
}