// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package assertions

import (
	"fmt"
	"runtime"
	"strings"
	"unicode"
	"unicode/utf8"
)

// Fail reports a failure through.
//
// # Usage
//
//	assertions.Fail(t, "failed")
//
// # Examples
//
//	failure: "failed"
func Fail(t T, failureMessage string, msgAndArgs ...any) bool {
	// Domain: testing
	if h, ok := t.(H); ok {
		h.Helper()
	}

	msgAndArgs, _ = splitArgs(msgAndArgs) // strip possible injected option
	if failureMessage != "" || len(msgAndArgs) > 0 {
		errorWithCallerInfo(t, 1, failureMessage, msgAndArgs...)
	}

	return false
}

// FailNow fails test.
//
// # Usage
//
//	assertions.FailNow(t, "failed")
//
// # Examples
//
//	failure: "failed"
func FailNow(t T, failureMessage string, msgAndArgs ...any) bool {
	// Domain: testing
	if h, ok := t.(H); ok {
		h.Helper()
	}

	Fail(t, failureMessage, msgAndArgs...)

	// We cannot extend T with FailNow() and
	// maintain backwards compatibility, so we fallback
	// to panicking when FailNow is not available in T.
	// See issue #263

	if t, ok := t.(failNower); ok {
		t.FailNow()
	} else {
		panic("test failed and t is missing `FailNow()`")
	}

	return false
}

// CallerInfo returns an array of strings containing the file and line number
// of each stack frame leading from the current test to the assert call that
// failed.
func CallerInfo() []string {
	// CallerInfo is necessary because the assert functions use the testing object
	// internally, causing it to print the file:line of the assert method, rather than where
	// the problem actually occurred in calling code.*/
	//
	// Maintainer: it is not necessary to export CallerInfo. This should remain an internal implementation detail.
	return callerInfo(1)
}

// Stolen from the `go test` tool.
// isTest tells whether name looks like a test (or benchmark, according to prefix).
// It is a Test (say) if there is a character after Test that is not a lower-case letter.
// We don't want TesticularCancer.
func isTest(name, prefix string) bool {
	if !strings.HasPrefix(name, prefix) {
		return false
	}
	if len(name) == len(prefix) { // "Test" is ok
		return true
	}
	r, _ := utf8.DecodeRuneInString(name[len(prefix):])
	return !unicode.IsLower(r)
}

func errorWithCallerInfo(t T, offset int, failureMessage string, msgAndArgs ...any) {
	if h, ok := t.(H); ok {
		h.Helper()
	}

	content := []labeledContent{
		{"Error Trace", strings.Join(callerInfo(offset), "\n\t\t\t")},
		{"Error", failureMessage},
	}

	// Add test name if the Go version supports it
	if n, ok := t.(namer); ok {
		content = append(content, labeledContent{"Test", n.Name()})
	}

	message := messageFromMsgAndArgs(msgAndArgs...)
	if len(message) > 0 {
		content = append(content, labeledContent{"Messages", message})
	}

	t.Errorf("\n%s", ""+labeledOutput(content...))
}

func callerInfo(offset int) []string {
	var (
		pc   uintptr
		file string
		line int
		name string
	)

	const stackFrameBufferSize = 10
	pcs := make([]uintptr, stackFrameBufferSize)

	callers := []string{}
	newOffset := offset

	for {
		n := runtime.Callers(newOffset, pcs)

		if n == 0 {
			break
		}

		frames := runtime.CallersFrames(pcs[:n])

		for {
			frame, more := frames.Next()
			pc = frame.PC
			file = frame.File
			line = frame.Line

			// This is a huge edge case, but it will panic if this is the case, see #180
			if file == "<autogenerated>" {
				break
			}

			f := runtime.FuncForPC(pc)
			if f == nil {
				break
			}
			name = f.Name()

			// testing.tRunner is the standard library function that calls
			// tests. Subtests are called directly by tRunner, without going through
			// the Test/Benchmark/Example function that contains the t.Run calls, so
			// with subtests we should break when we hit tRunner, without adding it
			// to the list of callers.
			if name == "testing.tRunner" {
				break
			}

			parts := strings.Split(file, "/")
			if len(parts) > 1 {
				filename := parts[len(parts)-1]
				dir := parts[len(parts)-2]
				if (dir != "assert" && dir != "mock" && dir != "require" && dir != "assertions") || filename == "mock_test.go" {
					callers = append(callers, fmt.Sprintf("%s:%d", file, line))
				}
			}

			// Drop the package
			dotPos := strings.LastIndexByte(name, '.')
			name = name[dotPos+1:]
			if isTest(name, "Test") ||
				isTest(name, "Benchmark") ||
				isTest(name, "Example") {
				break
			}

			if !more {
				break
			}
		}

		// Next batch
		newOffset += cap(pcs)
	}

	return callers
}
