package playlist

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

func TestParseAttributes(t *testing.T) {
	line := `#EXTINF:-1 tvg-id="123" tvg-name="Channel" group-title="News",Channel Name`
	attrs := parseAttributes(line)
	if attrs["tvg-id"] != "123" {
		t.Errorf("expected tvg-id=123, got %s", attrs["tvg-id"])
	}
	if attrs["tvg-name"] != "Channel" {
		t.Errorf("expected tvg-name=Channel, got %s", attrs["tvg-name"])
	}
	if attrs["group-title"] != "News" {
		t.Errorf("expected group-title=News, got %s", attrs["group-title"])
	}
}

func TestParseAttributes_Empty(t *testing.T) {
	attrs := parseAttributes("#EXTINF:-1,Plain Channel")
	if len(attrs) != 0 {
		t.Errorf("expected 0 attrs, got %d", len(attrs))
	}
}

func TestParseTitle_WithComma(t *testing.T) {
	cases := []struct {
		in   string
		want string
	}{
		{`#EXTINF:-1 tvg-id="x",My Channel`, "My Channel"},
		{`#EXTINF:-1,Hello, World`, "Hello, World"},
		{`#EXTINF:-1 tvg-name="Test",Test Channel`, "Test Channel"},
	}
	for _, c := range cases {
		got := parseTitle(c.in)
		if got != c.want {
			t.Errorf("parseTitle(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestParseTitle_NoComma(t *testing.T) {
	got := parseTitle(`#EXTINF:-1 tvg-name="Foo"`)
	if got == "" {
		t.Error("expected non-empty title")
	}
}

func TestParse_SimplePlaylist(t *testing.T) {
	content := `#EXTM3U
#EXTINF:-1 tvg-id="ch1" tvg-name="Channel 1" group-title="News",Channel 1
http://example.com/ch1.m3u8
#EXTINF:-1 tvg-id="ch2" tvg-name="Channel 2" group-title="Movies",Channel 2
http://example.com/ch2.m3u8
`
	pls := &Playlist{Content: content}
	pls.Parse()

	if len(pls.Channels) != 2 {
		t.Fatalf("expected 2 channels, got %d", len(pls.Channels))
	}
	if len(pls.Groups) != 2 {
		t.Fatalf("expected 2 groups, got %d", len(pls.Groups))
	}
}

func TestParse_WithBOM(t *testing.T) {
	content := "\xef\xbb\xbf#EXTM3U\n#EXTINF:-1,Test\nhttp://example.com/test.m3u8\n"
	pls := &Playlist{Content: content}
	pls.Parse()
	if len(pls.Channels) != 1 {
		t.Fatalf("expected 1 channel (BOM removed), got %d", len(pls.Channels))
	}
}

func TestParse_WithWindowsLineEndings(t *testing.T) {
	content := "#EXTM3U\r\n#EXTINF:-1,Test\r\nhttp://example.com/test.m3u8\r\n"
	pls := &Playlist{Content: content}
	pls.Parse()
	if len(pls.Channels) != 1 {
		t.Fatalf("expected 1 channel (CRLF), got %d", len(pls.Channels))
	}
}

func TestParse_EmptyContent(t *testing.T) {
	pls := &Playlist{Content: ""}
	pls.Parse()
	if len(pls.Channels) != 0 {
		t.Errorf("expected 0 channels, got %d", len(pls.Channels))
	}
}

func TestParse_Extgrp(t *testing.T) {
	content := `#EXTM3U
#EXTINF:-1,Channel A
#EXTGRP:Sports
http://example.com/a.m3u8
`
	pls := &Playlist{Content: content}
	pls.Parse()
	if len(pls.Groups) != 1 {
		t.Fatalf("expected 1 group, got %d", len(pls.Groups))
	}
	for _, ch := range pls.Channels {
		if ch.GroupId == "" {
			t.Error("expected non-empty group ID")
		}
	}
}

func TestParse_ExtinfAttributes(t *testing.T) {
	content := `#EXTM3U
#EXTINF:-1 tvg-id="abc" tvg-logo="logo.png" group-title="HD",HD Channel
http://example.com/hd.m3u8
`
	pls := &Playlist{Content: content}
	pls.Parse()
	for _, ch := range pls.Channels {
		if ch.Attributes["tvg-id"] != "abc" {
			t.Errorf("expected tvg-id=abc, got %s", ch.Attributes["tvg-id"])
		}
		if ch.Attributes["tvg-logo"] != "logo.png" {
			t.Errorf("expected tvg-logo=logo.png, got %s", ch.Attributes["tvg-logo"])
		}
		if ch.Title != "HD Channel" {
			t.Errorf("expected title 'HD Channel', got '%s'", ch.Title)
		}
	}
}

func TestParse_ChannelWithoutTitle(t *testing.T) {
	content := `#EXTM3U
#EXTINF:-1 tvg-id="notitle"
http://example.com/notitle.m3u8
`
	pls := &Playlist{Content: content}
	pls.Parse()
	for _, ch := range pls.Channels {
		if ch.Title == "" {
			t.Error("expected non-empty title for channel without name")
		}
	}
}

func TestParse_Extm3uAttributes(t *testing.T) {
	content := `#EXTM3U url-tvg="http://example.com/epg.xml"
#EXTINF:-1,Test
http://example.com/test.m3u8
`
	pls := &Playlist{Content: content}
	pls.Parse()
	if pls.Attributes["url-tvg"] != "http://example.com/epg.xml" {
		t.Errorf("expected url-tvg attr, got %s", pls.Attributes["url-tvg"])
	}
}

func TestMakeFromUrl(t *testing.T) {
	pls, err := MakeFromUrl("http://example.com/list.m3u8")
	if err != nil {
		t.Fatal(err)
	}
	if pls.Url != "http://example.com/list.m3u8" {
		t.Errorf("expected URL, got %s", pls.Url)
	}
	if pls.Source != "-u" {
		t.Errorf("expected source '-u', got '%s'", pls.Source)
	}
	if !pls.IsOnline {
		t.Error("expected IsOnline=true")
	}
}

func TestMakeFromFile_NotFound(t *testing.T) {
	_, err := MakeFromFile("/nonexistent/file.m3u")
	if err == nil {
		t.Error("expected error for non-existent file")
	}
}

func TestMakeFromFile_Success(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "test.m3u")
	content := "#EXTM3U\n#EXTINF:-1,Test\nhttp://example.com/test.m3u8\n"
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		t.Fatal(err)
	}
	pls, err := MakeFromFile(path)
	if err != nil {
		t.Fatal(err)
	}
	if pls.Source != "-f" {
		t.Errorf("expected source '-f', got '%s'", pls.Source)
	}
	if pls.Content != content {
		t.Error("content mismatch")
	}
}

func TestReadFromFs(t *testing.T) {
	tmp := t.TempDir()
	path := filepath.Join(tmp, "test.m3u")
	content := "#EXTM3U\n"
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		t.Fatal(err)
	}
	pls := &Playlist{Url: path}
	if err := pls.ReadFromFs(); err != nil {
		t.Fatal(err)
	}
	if pls.Content != content {
		t.Errorf("expected '%s', got '%s'", content, pls.Content)
	}
}

func TestReadFromFs_NotFound(t *testing.T) {
	pls := &Playlist{Url: "/nonexistent/file.m3u"}
	err := pls.ReadFromFs()
	if err == nil {
		t.Error("expected error for non-existent file")
	}
}

// TestParseConcurrent проверяет, что парсинг нескольких плейлистов
// в параллельных горутинах не приводит к потере каналов из-за data race.
// До фикса tmpChannel была пакетной переменной, и каналы терялись.
func TestParseConcurrent(t *testing.T) {
	const goroutines = 20
	const channelsPerPlaylist = 50

	// генерируем плейлист с channelsPerPlaylist каналами
	content := "#EXTM3U\n"
	for i := 0; i < channelsPerPlaylist; i++ {
		content += fmt.Sprintf("#EXTINF:-1 tvg-id=\"ch%d\",Channel %d\n", i, i)
		content += fmt.Sprintf("http://example.com/stream%d.m3u8\n", i)
	}

	var wg sync.WaitGroup
	wg.Add(goroutines)
	lost := make([]int, goroutines)

	for i := 0; i < goroutines; i++ {
		go func(idx int) {
			defer wg.Done()
			pls := &Playlist{Content: content}
			pls.Parse()
			lost[idx] = channelsPerPlaylist - len(pls.Channels)
		}(i)
	}
	wg.Wait()

	totalLost := 0
	for _, l := range lost {
		totalLost += l
	}
	if totalLost > 0 {
		t.Errorf("data race detected: %d channels lost across %d goroutines (per-goroutine losses: %v)",
			totalLost, goroutines, lost)
	}
}
