package astisub_test

import (
	"bytes"
	"os"
	"testing"
	"time"

	"github.com/asticode/go-astisub"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestLine_Text(t *testing.T) {
	var l = astisub.Line{Items: []astisub.LineItem{{Text: "1"}, {Text: "2"}, {Text: "3"}}}
	assert.Equal(t, "123", l.String())
}

func assertSubtitleItems(t *testing.T, i *astisub.Subtitles) {
	// No format
	assert.Len(t, i.Items, 6)
	assert.Equal(t, time.Minute+39*time.Second, i.Items[0].StartAt)
	assert.Equal(t, time.Minute+41*time.Second+40*time.Millisecond, i.Items[0].EndAt)
	assert.Equal(t, "(deep rumbling)", i.Items[0].Lines[0].String())
	assert.Equal(t, 2*time.Minute+4*time.Second+80*time.Millisecond, i.Items[1].StartAt)
	assert.Equal(t, 2*time.Minute+7*time.Second+120*time.Millisecond, i.Items[1].EndAt)
	assert.Equal(t, "MAN:", i.Items[1].Lines[0].String())
	assert.Equal(t, "How did we end up here?", i.Items[1].Lines[1].String())
	assert.Equal(t, 2*time.Minute+12*time.Second+160*time.Millisecond, i.Items[2].StartAt)
	assert.Equal(t, 2*time.Minute+15*time.Second+200*time.Millisecond, i.Items[2].EndAt)
	assert.Equal(t, "This place is horrible.", i.Items[2].Lines[0].String())
	assert.Equal(t, 2*time.Minute+20*time.Second+240*time.Millisecond, i.Items[3].StartAt)
	assert.Equal(t, 2*time.Minute+22*time.Second+280*time.Millisecond, i.Items[3].EndAt)
	assert.Equal(t, "Smells like balls.", i.Items[3].Lines[0].String())
	assert.Equal(t, 2*time.Minute+28*time.Second+320*time.Millisecond, i.Items[4].StartAt)
	assert.Equal(t, 2*time.Minute+31*time.Second+360*time.Millisecond, i.Items[4].EndAt)
	assert.Equal(t, "We don't belong", i.Items[4].Lines[0].String())
	assert.Equal(t, "in this shithole.", i.Items[4].Lines[1].String())
	assert.Equal(t, 2*time.Minute+31*time.Second+400*time.Millisecond, i.Items[5].StartAt)
	assert.Equal(t, 2*time.Minute+33*time.Second+440*time.Millisecond, i.Items[5].EndAt)
	assert.Equal(t, "(computer playing", i.Items[5].Lines[0].String())
	assert.Equal(t, "electronic melody)", i.Items[5].Lines[1].String())
}

func mockSubtitles() *astisub.Subtitles {
	return &astisub.Subtitles{Items: []*astisub.Item{{EndAt: 3 * time.Second, StartAt: time.Second, Lines: []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-1"}}}}}, {EndAt: 7 * time.Second, StartAt: 3 * time.Second, Lines: []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-2"}}}}}}}
}

func TestSubtitles_Add(t *testing.T) {
	var s = mockSubtitles()
	s.Add(time.Second)
	assert.Len(t, s.Items, 2)
	assert.Equal(t, 2*time.Second, s.Items[0].StartAt)
	assert.Equal(t, 4*time.Second, s.Items[0].EndAt)
	assert.Equal(t, 2*time.Second, s.Items[0].StartAt)
	assert.Equal(t, 4*time.Second, s.Items[0].EndAt)
	s.Add(-3 * time.Second)
	assert.Len(t, s.Items, 2)
	assert.Equal(t, time.Duration(0), s.Items[0].StartAt)
	assert.Equal(t, time.Second, s.Items[0].EndAt)
	s.Add(-2 * time.Second)
	assert.Len(t, s.Items, 1)
	assert.Equal(t, "subtitle-2", s.Items[0].Lines[0].Items[0].Text)
}

func TestSubtitles_Duration(t *testing.T) {
	assert.Equal(t, time.Duration(0), astisub.Subtitles{}.Duration())
	assert.Equal(t, 7*time.Second, mockSubtitles().Duration())
}

func TestSubtitles_IsEmpty(t *testing.T) {
	assert.True(t, astisub.Subtitles{}.IsEmpty())
	assert.False(t, mockSubtitles().IsEmpty())
}

func TestSubtitles_ForceDuration(t *testing.T) {
	var s = mockSubtitles()
	s.ForceDuration(10*time.Second, false)
	assert.Len(t, s.Items, 2)
	s = mockSubtitles()
	s.ForceDuration(10*time.Second, true)
	assert.Len(t, s.Items, 3)
	assert.Equal(t, 10*time.Second, s.Items[2].EndAt)
	assert.Equal(t, 10*time.Second-time.Millisecond, s.Items[2].StartAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "..."}}}}, s.Items[2].Lines)
	s.Items[2].StartAt = 7 * time.Second
	s.Items[2].EndAt = 12 * time.Second
	s.ForceDuration(10*time.Second, true)
	assert.Len(t, s.Items, 3)
	assert.Equal(t, 10*time.Second, s.Items[2].EndAt)
	assert.Equal(t, 7*time.Second, s.Items[2].StartAt)
}

func TestSubtitles_Fragment(t *testing.T) {
	// Init
	var s = mockSubtitles()

	// Fragment
	s.Fragment(2 * time.Second)
	assert.Len(t, s.Items, 5)
	assert.Equal(t, time.Second, s.Items[0].StartAt)
	assert.Equal(t, 2*time.Second, s.Items[0].EndAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-1"}}}}, s.Items[0].Lines)
	assert.Equal(t, 2*time.Second, s.Items[1].StartAt)
	assert.Equal(t, 3*time.Second, s.Items[1].EndAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-1"}}}}, s.Items[1].Lines)
	assert.Equal(t, 3*time.Second, s.Items[2].StartAt)
	assert.Equal(t, 4*time.Second, s.Items[2].EndAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-2"}}}}, s.Items[2].Lines)
	assert.Equal(t, 4*time.Second, s.Items[3].StartAt)
	assert.Equal(t, 6*time.Second, s.Items[3].EndAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-2"}}}}, s.Items[3].Lines)
	assert.Equal(t, 6*time.Second, s.Items[4].StartAt)
	assert.Equal(t, 7*time.Second, s.Items[4].EndAt)
	assert.Equal(t, []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-2"}}}}, s.Items[4].Lines)

	// Unfragment
	s.Items = append(s.Items[:4], append([]*astisub.Item{{EndAt: 5 * time.Second, Lines: []astisub.Line{{Items: []astisub.LineItem{{Text: "subtitle-3"}}}}, StartAt: 4 * time.Second}}, s.Items[4:]...)...)
	s.Unfragment()
	assert.Len(t, s.Items, 3)
	assert.Equal(t, "subtitle-1", s.Items[0].String())
	assert.Equal(t, time.Second, s.Items[0].StartAt)
	assert.Equal(t, 3*time.Second, s.Items[0].EndAt)
	assert.Equal(t, "subtitle-2", s.Items[1].String())
	assert.Equal(t, 3*time.Second, s.Items[1].StartAt)
	assert.Equal(t, 7*time.Second, s.Items[1].EndAt)
	assert.Equal(t, "subtitle-3", s.Items[2].String())
	assert.Equal(t, 4*time.Second, s.Items[2].StartAt)
	assert.Equal(t, 5*time.Second, s.Items[2].EndAt)
}

func TestSubtitles_Unfragment(t *testing.T) {
	itemText := func(s string) []astisub.Line {
		return []astisub.Line{{Items: []astisub.LineItem{{Text: s}}}}
	}
	items := []*astisub.Item{{
		Lines:   itemText("subtitle-1"),
		StartAt: 1 * time.Second,
		EndAt:   2 * time.Second,
	}, {
		Lines:   itemText("subtitle-2"),
		StartAt: 2 * time.Second,
		EndAt:   5 * time.Second,
	}, {
		Lines:   itemText("subtitle-3"),
		StartAt: 3 * time.Second,
		EndAt:   4 * time.Second,
	}, {
		// gap and nested within first subtitle-2; should not override end time
		Lines:   itemText("subtitle-2"),
		StartAt: 3 * time.Second,
		EndAt:   4 * time.Second,
	}, {
		Lines: itemText("subtitle-3"),
		// gap and start time equals previous end time
		StartAt: 4 * time.Second,
		EndAt:   5 * time.Second,
	}, {
		// should not be combined
		Lines:   itemText("subtitle-3"),
		StartAt: 6 * time.Second,
		EndAt:   7 * time.Second,
	}, {
		// test correcting for out-of-orderness
		Lines:   itemText("subtitle-1"),
		StartAt: 0 * time.Second,
		EndAt:   3 * time.Second,
	}}

	s := &astisub.Subtitles{Items: items}

	s.Unfragment()

	expected := []astisub.Item{{
		Lines:   itemText("subtitle-1"),
		StartAt: 0 * time.Second,
		EndAt:   3 * time.Second,
	}, {
		Lines:   itemText("subtitle-2"),
		StartAt: 2 * time.Second,
		EndAt:   5 * time.Second,
	}, {
		Lines:   itemText("subtitle-3"),
		StartAt: 3 * time.Second,
		EndAt:   5 * time.Second,
	}, {
		Lines:   itemText("subtitle-3"),
		StartAt: 6 * time.Second,
		EndAt:   7 * time.Second,
	}}

	assert.Equal(t, len(expected), len(s.Items))
	for i := range expected {
		assert.Equal(t, expected[i], *s.Items[i])
	}
}

func TestSubtitles_Merge(t *testing.T) {
	var s1 = &astisub.Subtitles{Items: []*astisub.Item{{EndAt: 3 * time.Second, StartAt: time.Second}, {EndAt: 8 * time.Second, StartAt: 5 * time.Second}, {EndAt: 12 * time.Second, StartAt: 10 * time.Second}}, Regions: map[string]*astisub.Region{"region_0": {ID: "region_0"}, "region_1": {ID: "region_1"}}, Styles: map[string]*astisub.Style{"style_0": {ID: "style_0"}, "style_1": {ID: "style_1"}}}
	var s2 = &astisub.Subtitles{Items: []*astisub.Item{{EndAt: 4 * time.Second, StartAt: 2 * time.Second}, {EndAt: 7 * time.Second, StartAt: 6 * time.Second}, {EndAt: 11 * time.Second, StartAt: 9 * time.Second}, {EndAt: 14 * time.Second, StartAt: 13 * time.Second}}, Regions: map[string]*astisub.Region{"region_1": {ID: "region_1"}, "region_2": {ID: "region_2"}}, Styles: map[string]*astisub.Style{"style_1": {ID: "style_1"}, "style_2": {ID: "style_2"}}}
	s1.Merge(s2)
	assert.Len(t, s1.Items, 7)
	assert.Equal(t, &astisub.Item{EndAt: 3 * time.Second, StartAt: time.Second}, s1.Items[0])
	assert.Equal(t, &astisub.Item{EndAt: 4 * time.Second, StartAt: 2 * time.Second}, s1.Items[1])
	assert.Equal(t, &astisub.Item{EndAt: 8 * time.Second, StartAt: 5 * time.Second}, s1.Items[2])
	assert.Equal(t, &astisub.Item{EndAt: 7 * time.Second, StartAt: 6 * time.Second}, s1.Items[3])
	assert.Equal(t, &astisub.Item{EndAt: 11 * time.Second, StartAt: 9 * time.Second}, s1.Items[4])
	assert.Equal(t, &astisub.Item{EndAt: 12 * time.Second, StartAt: 10 * time.Second}, s1.Items[5])
	assert.Equal(t, &astisub.Item{EndAt: 14 * time.Second, StartAt: 13 * time.Second}, s1.Items[6])
	assert.Equal(t, len(s1.Regions), 3)
	assert.Equal(t, len(s1.Styles), 3)
}

func TestSubtitles_Optimize(t *testing.T) {
	var s = &astisub.Subtitles{
		Items: []*astisub.Item{
			{Region: &astisub.Region{ID: "1"}},
			{Style: &astisub.Style{ID: "1"}},
			{Lines: []astisub.Line{{Items: []astisub.LineItem{{Style: &astisub.Style{ID: "2"}}}}}},
		},
		Regions: map[string]*astisub.Region{
			"1": {ID: "1", Style: &astisub.Style{ID: "3"}},
			"2": {ID: "2", Style: &astisub.Style{ID: "4"}},
		},
		Styles: map[string]*astisub.Style{
			"1": {ID: "1"},
			"2": {ID: "2"},
			"3": {ID: "3"},
			"4": {ID: "4"},
			"5": {ID: "5"},
		},
	}
	s.Optimize()
	assert.Len(t, s.Regions, 1)
	assert.Len(t, s.Styles, 3)
}

func TestSubtitles_Order(t *testing.T) {
	var s = &astisub.Subtitles{Items: []*astisub.Item{{StartAt: 4 * time.Second, EndAt: 5 * time.Second}, {StartAt: 2 * time.Second, EndAt: 3 * time.Second}, {StartAt: 3 * time.Second, EndAt: 4 * time.Second}, {StartAt: time.Second, EndAt: 2 * time.Second}}}
	s.Order()
	assert.Equal(t, time.Second, s.Items[0].StartAt)
	assert.Equal(t, 2*time.Second, s.Items[0].EndAt)
	assert.Equal(t, 2*time.Second, s.Items[1].StartAt)
	assert.Equal(t, 3*time.Second, s.Items[1].EndAt)
	assert.Equal(t, 3*time.Second, s.Items[2].StartAt)
	assert.Equal(t, 4*time.Second, s.Items[2].EndAt)
	assert.Equal(t, 4*time.Second, s.Items[3].StartAt)
	assert.Equal(t, 5*time.Second, s.Items[3].EndAt)
}

func TestSubtitles_RemoveStyling(t *testing.T) {
	s := &astisub.Subtitles{
		Items: []*astisub.Item{
			{
				Lines: []astisub.Line{{
					Items: []astisub.LineItem{{
						InlineStyle: &astisub.StyleAttributes{},
						Style:       &astisub.Style{},
					}},
				}},
				InlineStyle: &astisub.StyleAttributes{},
				Region:      &astisub.Region{},
				Style:       &astisub.Style{},
			},
		},
		Regions: map[string]*astisub.Region{"region": {}},
		Styles:  map[string]*astisub.Style{"style": {}},
	}
	s.RemoveStyling()
	assert.Equal(t, &astisub.Subtitles{
		Items: []*astisub.Item{
			{
				Lines: []astisub.Line{{
					Items: []astisub.LineItem{{}},
				}},
			},
		},
		Regions: map[string]*astisub.Region{},
		Styles:  map[string]*astisub.Style{},
	}, s)
}

func TestSubtitles_ApplyLinearCorrection(t *testing.T) {
	s := &astisub.Subtitles{Items: []*astisub.Item{
		{
			EndAt:   2 * time.Second,
			StartAt: 1 * time.Second,
		},
		{
			EndAt:   5 * time.Second,
			StartAt: 3 * time.Second,
		},
		{
			EndAt:   10 * time.Second,
			StartAt: 7 * time.Second,
		},
	}}
	s.ApplyLinearCorrection(3*time.Second, 5*time.Second, 5*time.Second, 8*time.Second)
	require.Equal(t, 2*time.Second, s.Items[0].StartAt)
	require.Equal(t, 3500*time.Millisecond, s.Items[0].EndAt)
	require.Equal(t, 5*time.Second, s.Items[1].StartAt)
	require.Equal(t, 8*time.Second, s.Items[1].EndAt)
	require.Equal(t, 11*time.Second, s.Items[2].StartAt)
	require.Equal(t, 15500*time.Millisecond, s.Items[2].EndAt)
}

func TestHTMLEntity(t *testing.T) {
	exts := []string{"srt", "vtt"}
	for _, ext := range exts {
		// Read input with entities
		s, err := astisub.OpenFile("./testdata/example-in-html-entities." + ext)
		assert.NoError(t, err)

		assert.Len(t, s.Items, 3)
		assert.Equal(t, 331*time.Millisecond, s.Items[0].StartAt)
		assert.Equal(t, 3*time.Second+750*time.Millisecond, s.Items[0].EndAt)
		assert.Equal(t, "The man in black fled across the desert, \u00A0", s.Items[0].Lines[0].String())
		assert.Equal(t, "& the gunslinger followed.", s.Items[0].Lines[1].String())
		assert.Equal(t, 4*time.Second+101*time.Millisecond, s.Items[1].StartAt)
		assert.Equal(t, 5*time.Second+430*time.Millisecond, s.Items[1].EndAt)
		assert.Equal(t, "Go,\u00A0then,", s.Items[1].Lines[0].String())
		assert.Equal(t, 6*time.Second+331*time.Millisecond, s.Items[2].StartAt)
		assert.Equal(t, 9*time.Second+675*time.Millisecond, s.Items[2].EndAt)
		assert.Equal(t, "there are other < worlds than these.", s.Items[2].Lines[0].String())

		//Write to srt
		w := &bytes.Buffer{}
		c, err := os.ReadFile("./testdata/example-out-html-entities.srt")
		assert.NoError(t, err)
		err = s.WriteToSRT(w)
		assert.NoError(t, err)
		assert.Equal(t, string(c), w.String())

		//Write WebVTT
		w = &bytes.Buffer{}
		c, err = os.ReadFile("./testdata/example-out-html-entities.vtt")
		assert.NoError(t, err)
		err = s.WriteToWebVTT(w)
		assert.NoError(t, err)
		assert.Equal(t, string(c), w.String())
	}
}

func TestNewScanner(t *testing.T) {
	exts := []string{"vtt", "srt", "ssa"}
	for _, ext := range exts {
		s, err := astisub.OpenFile("./testdata/example-in-carriage-return." + ext)
		assert.NoError(t, err)
		assert.Len(t, s.Items, 3)
		assert.Equal(t, time.Duration(0), s.Items[0].StartAt)
		assert.Equal(t, 3*time.Second+766*time.Millisecond, s.Items[0].EndAt)
		assert.Equal(t, "Did one of the last stories strike you as", s.Items[0].Lines[0].String())
		assert.Equal(t, "more interesting than the other?", s.Items[0].Lines[1].String())

		assert.Equal(t, 3*time.Second+767*time.Millisecond, s.Items[1].StartAt)
		assert.Equal(t, 10*time.Second+732*time.Millisecond, s.Items[1].EndAt)
		assert.Equal(t, "That's true. You don’t often find 632", s.Items[1].Lines[0].String())
		assert.Equal(t, "pieces of gum stuck on a sidewalk", s.Items[1].Lines[1].String())

		assert.Equal(t, 10*time.Second+733*time.Millisecond, s.Items[2].StartAt)
		assert.Equal(t, 14*time.Second+66*time.Millisecond, s.Items[2].EndAt)
		assert.Equal(t, "at a busy bus stop or anywhere", s.Items[2].Lines[0].String())
		assert.Equal(t, "else for that matter.", s.Items[2].Lines[1].String())
	}
}
