package mpb_test

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

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

func TestBarCompleted(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	total := 80
	bar := p.AddBar(int64(total))

	if bar.Completed() {
		t.Fail()
	}

	bar.IncrBy(total)

	if !bar.Completed() {
		t.Error("bar isn't completed after increment")
	}

	p.Wait()
}

func TestBarAborted(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	total := 80
	bar := p.AddBar(int64(total))

	if bar.Aborted() {
		t.Fail()
	}

	bar.Abort(false)

	if !bar.Aborted() {
		t.Error("bar isn't aborted after abort call")
	}

	p.Wait()
}

func TestBarSetTotal(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	bar := p.AddBar(0)

	bar.SetTotal(0, false)
	if bar.Completed() {
		t.Error("expected bar not to complete")
	}

	bar.SetTotal(0, true)
	if !bar.Completed() {
		t.Error("expected bar to complete")
	}

	p.Wait()
}

func TestBarEnableTriggerCompleteAndIncrementBefore(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	bar := p.AddBar(0) // never complete bar

	for _, f := range []func(){
		func() { bar.SetTotal(40, false) },
		func() { bar.IncrBy(60) },
		func() { bar.SetTotal(80, false) },
		func() { bar.IncrBy(20) },
	} {
		f()
		if bar.Completed() {
			t.Fail()
		}
	}

	bar.EnableTriggerComplete()

	if !bar.Completed() {
		t.Fail()
	}

	p.Wait()
}

func TestBarEnableTriggerCompleteAndIncrementAfter(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	bar := p.AddBar(0) // never complete bar

	for _, f := range []func(){
		func() { bar.SetTotal(40, false) },
		func() { bar.IncrBy(60) },
		func() { bar.SetTotal(80, false) },
		func() { bar.EnableTriggerComplete() },
	} {
		f()
		if bar.Completed() {
			t.Fail()
		}
	}

	bar.IncrBy(20)

	if !bar.Completed() {
		t.Fail()
	}

	p.Wait()
}

func TestBarID(t *testing.T) {
	p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard))
	total := 100
	wantID := 11
	bar := p.AddBar(int64(total), mpb.BarID(wantID))

	gotID := bar.ID()
	if gotID != wantID {
		t.Errorf("Expected bar id: %d, got %d", wantID, gotID)
	}

	bar.IncrBy(total)

	p.Wait()
}

func TestBarSetRefill(t *testing.T) {
	var buf bytes.Buffer
	p := mpb.New(
		mpb.WithWidth(100),
		mpb.WithOutput(&buf),
		mpb.WithAutoRefresh(),
	)

	total := 100
	till := 30
	refiller := "+"

	bar := p.New(int64(total), mpb.BarStyle().Refiller(refiller), mpb.BarFillerTrim())

	bar.IncrBy(till)
	bar.SetRefill(int64(till))
	bar.IncrBy(total - till)

	p.Wait()

	wantBar := fmt.Sprintf("[%s%s]",
		strings.Repeat(refiller, till-1),
		strings.Repeat("=", total-till-1),
	)

	got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0])

	if !strings.Contains(got, wantBar) {
		t.Errorf("Want bar: %q, got bar: %q", wantBar, got)
	}
}

func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) {
	var buf bytes.Buffer
	p := mpb.New(
		mpb.WithWidth(80),
		mpb.WithOutput(&buf),
		mpb.WithAutoRefresh(),
	)

	total := 50

	bar := p.AddBar(int64(total),
		mpb.BarRemoveOnComplete(),
		mpb.AppendDecorators(decor.Percentage()),
	)

	bar.IncrBy(total)

	p.Wait()

	hundred := "100 %"
	if !bytes.Contains(buf.Bytes(), []byte(hundred)) {
		t.Errorf("Bar's buffer does not contain: %q", hundred)
	}
}

func TestBarStyle(t *testing.T) {
	var buf bytes.Buffer
	customFormat := "╢▌▌░╟"
	runes := []rune(customFormat)
	total := 80
	p := mpb.New(
		mpb.WithWidth(80),
		mpb.WithOutput(&buf),
		mpb.WithAutoRefresh(),
	)
	bs := mpb.BarStyle()
	bs = bs.Lbound(string(runes[0]))
	bs = bs.Filler(string(runes[1]))
	bs = bs.Tip(string(runes[2]))
	bs = bs.Padding(string(runes[3]))
	bs = bs.Rbound(string(runes[4]))
	bar := p.New(int64(total), bs, mpb.BarFillerTrim())

	bar.IncrBy(total)

	p.Wait()

	wantBar := fmt.Sprintf("%s%s%s%s",
		string(runes[0]),
		strings.Repeat(string(runes[1]), total-3),
		string(runes[2]),
		string(runes[4]),
	)
	got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0])

	if !strings.Contains(got, wantBar) {
		t.Errorf("Want bar: %q:%d, got bar: %q:%d", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got))
	}
}

func TestDecorStatisticsAvailableWidth(t *testing.T) {
	ch := make(chan int, 2)
	td1 := func(s decor.Statistics) string {
		ch <- s.AvailableWidth
		return strings.Repeat("0", 20)
	}
	td2 := func(s decor.Statistics) string {
		ch <- s.AvailableWidth
		return ""
	}
	ctx, cancel := context.WithCancel(context.Background())
	refresh := make(chan interface{})
	p := mpb.NewWithContext(ctx,
		mpb.WithWidth(100),
		mpb.WithManualRefresh(refresh),
		mpb.WithOutput(io.Discard),
	)
	_ = p.AddBar(0,
		mpb.BarFillerTrim(),
		mpb.PrependDecorators(
			decor.Name(strings.Repeat("0", 20)),
			decor.Meta(
				decor.Any(td1),
				func(s string) string {
					return "\x1b[31;1m" + s + "\x1b[0m"
				},
			),
		),
		mpb.AppendDecorators(
			decor.Name(strings.Repeat("0", 20)),
			decor.Any(td2),
		),
	)
	refresh <- time.Now()
	go func() {
		time.Sleep(10 * time.Millisecond)
		cancel()
	}()
	p.Wait()

	if availableWidth := <-ch; availableWidth != 80 {
		t.Errorf("expected AvailableWidth %d got %d", 80, availableWidth)
	}

	if availableWidth := <-ch; availableWidth != 40 {
		t.Errorf("expected AvailableWidth %d got %d", 40, availableWidth)
	}
}

func TestBarQueueAfterBar(t *testing.T) {
	shutdown := make(chan interface{})
	ctx, cancel := context.WithCancel(context.Background())
	p := mpb.NewWithContext(ctx,
		mpb.WithOutput(io.Discard),
		mpb.WithAutoRefresh(),
		mpb.WithShutdownNotifier(shutdown),
	)
	a := p.AddBar(100)
	b := p.AddBar(100, mpb.BarQueueAfter(a))
	identity := map[*mpb.Bar]string{
		a: "a",
		b: "b",
	}

	a.IncrBy(100)
	a.Wait()
	cancel()

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

	p.Wait()
	if bars[0] != b {
		t.Errorf("Expected bars[0] == b, got: %s", identity[bars[0]])
	}
}
