// Copyright ©2015 The Gonum 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 optimize

import (
	"fmt"
	"math"
	"reflect"
	"testing"

	"gonum.org/v1/gonum/optimize/functions"
)

func TestMoreThuente(t *testing.T) {
	d := 0.001
	c := 0.001
	ls := &MoreThuente{
		DecreaseFactor:  d,
		CurvatureFactor: c,
	}
	testLinesearcher(t, ls, d, c, true)
}

func TestBisection(t *testing.T) {
	c := 0.1
	ls := &Bisection{
		CurvatureFactor: c,
	}
	testLinesearcher(t, ls, 0, c, true)
}

func TestBacktracking(t *testing.T) {
	d := 0.001
	ls := &Backtracking{
		DecreaseFactor: d,
	}
	testLinesearcher(t, ls, d, 0, false)
}

type funcGrader interface {
	Func([]float64) float64
	Grad([]float64, []float64) []float64
}

type linesearcherTest struct {
	name string
	f    func(float64) float64
	g    func(float64) float64
}

func newLinesearcherTest(name string, fg funcGrader) linesearcherTest {
	grad := make([]float64, 1)
	return linesearcherTest{
		name: name,
		f: func(x float64) float64 {
			return fg.Func([]float64{x})
		},
		g: func(x float64) float64 {
			fg.Grad(grad, []float64{x})
			return grad[0]
		},
	}
}

func testLinesearcher(t *testing.T, ls Linesearcher, decrease, curvature float64, strongWolfe bool) {
	for i, prob := range []linesearcherTest{
		newLinesearcherTest("Concave-to-the-right function", functions.ConcaveRight{}),
		newLinesearcherTest("Concave-to-the-left function", functions.ConcaveLeft{}),
		newLinesearcherTest("Plassmann wiggly function (l=39, beta=0.01)", functions.Plassmann{L: 39, Beta: 0.01}),
		newLinesearcherTest("Yanai-Ozawa-Kaneko function (beta1=0.001, beta2=0.001)", functions.YanaiOzawaKaneko{Beta1: 0.001, Beta2: 0.001}),
		newLinesearcherTest("Yanai-Ozawa-Kaneko function (beta1=0.01, beta2=0.001)", functions.YanaiOzawaKaneko{Beta1: 0.01, Beta2: 0.001}),
		newLinesearcherTest("Yanai-Ozawa-Kaneko function (beta1=0.001, beta2=0.01)", functions.YanaiOzawaKaneko{Beta1: 0.001, Beta2: 0.01}),
	} {
		for _, initStep := range []float64{0.001, 0.1, 1, 10, 1000} {
			prefix := fmt.Sprintf("test %d (%v started from %v)", i, prob.name, initStep)

			f0 := prob.f(0)
			g0 := prob.g(0)
			if g0 >= 0 {
				panic("bad test function")
			}

			op := ls.Init(f0, g0, initStep)
			if !op.isEvaluation() {
				t.Errorf("%v: Linesearcher.Init returned non-evaluating operation %v", prefix, op)
				continue
			}

			var (
				err  error
				k    int
				f, g float64
				step float64
			)
		loop:
			for {
				switch op {
				case MajorIteration:
					if f > f0+step*decrease*g0 {
						t.Errorf("%v: %v found step %v that does not satisfy the sufficient decrease condition",
							prefix, reflect.TypeOf(ls), step)
					}
					if strongWolfe && math.Abs(g) > curvature*(-g0) {
						t.Errorf("%v: %v found step %v that does not satisfy the curvature condition",
							prefix, reflect.TypeOf(ls), step)
					}
					break loop
				case FuncEvaluation:
					f = prob.f(step)
				case GradEvaluation:
					g = prob.g(step)
				case FuncEvaluation | GradEvaluation:
					f = prob.f(step)
					g = prob.g(step)
				default:
					t.Errorf("%v: Linesearcher returned an invalid operation %v", prefix, op)
					break loop
				}

				k++
				if k == 1000 {
					t.Errorf("%v: %v did not finish", prefix, reflect.TypeOf(ls))
					break
				}

				op, step, err = ls.Iterate(f, g)
				if err != nil {
					t.Errorf("%v: %v failed at step %v with %v", prefix, reflect.TypeOf(ls), step, err)
					break
				}
			}
		}
	}
}
