package mpb_test

import (
	"bytes"
	"container/heap"
	"context"
	"errors"
	"io"
	"strings"
	"testing"
	"time"

	"github.com/vbauerster/mpb/v8"
	"github.com/vbauerster/mpb/v8/decor"
)

const (
	timeout = 300 * time.Millisecond
)

func TestWithContext(t *testing.T) {
	shutdown := make(chan interface{})
	ctx, cancel := context.WithCancel(context.Background())
	p := mpb.NewWithContext(ctx,
		mpb.WithOutput(io.Discard),
		mpb.WithShutdownNotifier(shutdown),
	)
	_ = p.AddBar(0) // never complete bar
	_ = p.AddBar(0) // never complete bar
	go func() {
		time.Sleep(10 * time.Millisecond)
		cancel()
	}()

	p.Wait()

	select {
	case v := <-shutdown:
		if l := len(v.([]*mpb.Bar)); l != 2 {
			t.Errorf("Expected len of bars: %d, got: %d", 2, l)
		}
	case <-time.After(timeout):
		t.Errorf("Progress didn't shutdown after %v", timeout)
	}
}

func TestShutdownsWithErrFiller(t *testing.T) {
	var debug bytes.Buffer
	shutdown := make(chan interface{})
	p := mpb.New(
		mpb.WithShutdownNotifier(shutdown),
		mpb.WithOutput(io.Discard),
		mpb.WithDebugOutput(&debug),
		mpb.WithAutoRefresh(),
	)

	var errReturnCount int
	testError := errors.New("test error")
	bar := p.AddBar(100,
		mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller {
			return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error {
				if st.Current >= 22 {
					errReturnCount++
					return testError
				}
				return base.Fill(w, st)
			})
		}),
	)

	go func() {
		for bar.IsRunning() {
			bar.Increment()
			time.Sleep(10 * time.Millisecond)
		}
	}()

	p.Wait()

	if errReturnCount != 1 {
		t.Errorf("Expected errReturnCount: %d, got: %d\n", 1, errReturnCount)
	}

	select {
	case v := <-shutdown:
		if l := len(v.([]*mpb.Bar)); l != 0 {
			t.Errorf("Expected len of bars: %d, got: %d\n", 0, l)
		}
		if err := strings.TrimSpace(debug.String()); err != testError.Error() {
			t.Errorf("Expected err: %q, got %q\n", testError.Error(), err)
		}
	case <-time.After(timeout):
		t.Errorf("Progress didn't shutdown after %v", timeout)
	}
}

func TestShutdownAfterBarAbortWithDrop(t *testing.T) {
	shutdown := make(chan interface{})
	p := mpb.New(
		mpb.WithShutdownNotifier(shutdown),
		mpb.WithOutput(io.Discard),
		mpb.WithAutoRefresh(),
	)
	b := p.AddBar(100)

	var count int
	for i := 0; !b.Aborted(); i++ {
		if i >= 10 {
			count++
			b.Abort(true)
		} else {
			b.Increment()
			time.Sleep(10 * time.Millisecond)
		}
	}

	p.Wait()

	if count != 1 {
		t.Errorf("Expected count: %d, got: %d", 1, count)
	}

	select {
	case v := <-shutdown:
		if l := len(v.([]*mpb.Bar)); l != 0 {
			t.Errorf("Expected len of bars: %d, got: %d", 0, l)
		}
	case <-time.After(timeout):
		t.Errorf("Progress didn't shutdown after %v", timeout)
	}
}

func TestShutdownAfterBarAbortWithNoDrop(t *testing.T) {
	shutdown := make(chan interface{})
	p := mpb.New(
		mpb.WithShutdownNotifier(shutdown),
		mpb.WithOutput(io.Discard),
		mpb.WithAutoRefresh(),
	)
	b := p.AddBar(100)

	var count int
	for i := 0; !b.Aborted(); i++ {
		if i >= 10 {
			count++
			b.Abort(false)
		} else {
			b.Increment()
			time.Sleep(10 * time.Millisecond)
		}
	}

	p.Wait()

	if count != 1 {
		t.Errorf("Expected count: %d, got: %d", 1, count)
	}

	select {
	case v := <-shutdown:
		if l := len(v.([]*mpb.Bar)); l != 1 {
			t.Errorf("Expected len of bars: %d, got: %d", 1, l)
		}
	case <-time.After(timeout):
		t.Errorf("Progress didn't shutdown after %v", timeout)
	}
}

func TestBarPristinePopOrder(t *testing.T) {
	shutdown := make(chan interface{})
	ctx, cancel := context.WithCancel(context.Background())
	p := mpb.NewWithContext(ctx,
		mpb.WithOutput(io.Discard), // auto refresh is disabled
		mpb.WithShutdownNotifier(shutdown),
	)
	a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1))
	b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2))
	c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3))
	pristineOrder := []*mpb.Bar{c, b, a}

	go cancel()

	bars := (<-shutdown).([]*mpb.Bar)
	if l := len(bars); l != 3 {
		t.Fatalf("Expected len of bars: %d, got: %d", 3, l)
	}

	p.Wait()
	pq := mpb.PriorityQueue(bars)

	for _, b := range pristineOrder {
		// higher priority pops first
		if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() {
			t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID())
		}
	}
}

func makeUpdateBarPriorityTest(refresh, lazy bool) func(*testing.T) {
	return func(t *testing.T) {
		shutdown := make(chan interface{})
		refreshCh := make(chan interface{})
		ctx, cancel := context.WithCancel(context.Background())
		p := mpb.NewWithContext(ctx,
			mpb.WithOutput(io.Discard),
			mpb.WithManualRefresh(refreshCh),
			mpb.WithShutdownNotifier(shutdown),
		)
		a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1))
		b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2))
		c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3))

		p.UpdateBarPriority(c, 2, lazy)
		p.UpdateBarPriority(b, 3, lazy)
		checkOrder := []*mpb.Bar{b, c, a} // updated order

		if refresh {
			refreshCh <- time.Now()
		} else if lazy {
			checkOrder = []*mpb.Bar{c, b, a} // pristine order
		}

		go cancel()

		bars := (<-shutdown).([]*mpb.Bar)
		if l := len(bars); l != 3 {
			t.Fatalf("Expected len of bars: %d, got: %d", 3, l)
		}

		p.Wait()
		pq := mpb.PriorityQueue(bars)

		for _, b := range checkOrder {
			// higher priority pops first
			if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() {
				t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID())
			}
		}
	}
}

func TestUpdateBarPriority(t *testing.T) {
	makeUpdateBarPriorityTest(false, false)(t)
	makeUpdateBarPriorityTest(true, false)(t)
}

func TestUpdateBarPriorityLazy(t *testing.T) {
	makeUpdateBarPriorityTest(false, true)(t)
	makeUpdateBarPriorityTest(true, true)(t)
}

func TestNoOutput(t *testing.T) {
	var buf bytes.Buffer
	p := mpb.New(mpb.WithOutput(&buf))
	bar := p.AddBar(100)

	go func() {
		for !bar.Completed() {
			bar.Increment()
		}
	}()

	p.Wait()

	if buf.Len() != 0 {
		t.Errorf("Expected buf.Len == 0, got: %d\n", buf.Len())
	}
}

func TestAddAfterDone(t *testing.T) {
	p := mpb.New(mpb.WithOutput(io.Discard))
	bar := p.AddBar(100)
	bar.IncrBy(100)

	p.Wait()

	_, err := p.Add(100, nil)

	if err != mpb.DoneError {
		t.Errorf("Expected %q, got: %q\n", mpb.DoneError, err)
	}
}
