package web

import (
	"axenov/iptv-checker/app/config"
	"axenov/iptv-checker/app/playlist"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// makeTestServer создаёт HTTP-сервер с тестовыми плейлистами в memCache.
// Возвращает сервер и обёртку для добавления плейлистов в memCache.
func makeTestServer(t *testing.T) (*httptest.Server, *Server) {
	t.Helper()
	cfg := config.Init("")
	cfg.App.Playlists = "testdata/playlists.ini"

	srv := NewServer(cfg, nil)

	mux := http.NewServeMux()
	// API routes
	mux.HandleFunc("GET /api/playlists", srv.handleAPIGetAll)
	mux.HandleFunc("GET /api/playlists/{code}", srv.handleAPIGetOne)
	mux.HandleFunc("GET /api/playlists/{code}/channels", srv.handleAPIGetChannels)
	mux.HandleFunc("GET /api/version", srv.handleAPIVersion)
	mux.HandleFunc("GET /api/health", srv.handleAPIHealth)
	mux.HandleFunc("GET /api/stats", srv.handleAPIStats)
	mux.HandleFunc("GET /api/openapi.json", srv.handleOpenAPISpec)
	mux.HandleFunc("GET /api", func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/api/", http.StatusMovedPermanently)
	})
	mux.Handle("GET /api/", http.StripPrefix("/api/", swaggerFileServer))
	// page routes
	mux.HandleFunc("GET /{$}", srv.handleHome)
	mux.HandleFunc("GET /{path...}", srv.handleRoute)
	ts := httptest.NewServer(mux)
	t.Cleanup(ts.Close)
	return ts, srv
}

// putPlaylist кладёт плейлист в in-memory кеш сервера
func (s *Server) putPlaylist(pls playlist.Playlist) {
	s.memMu.Lock()
	defer s.memMu.Unlock()
	if s.memCache == nil {
		s.memCache = make(map[string]playlist.Playlist)
	}
	s.memCache[pls.Code] = pls
}

// makeOnlinePlaylist создаёт тестовый плейлист с онлайн/оффлайн каналами
func makeOnlinePlaylist() playlist.Playlist {
	return playlist.Playlist{
		Code:        "test1",
		Name:        "Test Playlist 1",
		Description: "A test playlist",
		Url:         "http://example.com/test1.m3u8",
		Source:      "test source",
		IsOnline:    true,
		CheckedAt:   1700000000,
		Attributes:  map[string]string{"url-tvg": "http://example.com/epg.xml"},
		Groups: map[string]playlist.Group{
			"group1": {Id: "group1", Name: "Group One"},
			"group2": {Id: "group2", Name: "Group Two"},
		},
		Channels: map[string]playlist.Channel{
			"id1": {
				Id:         "id1",
				Title:      "Channel One",
				URL:        "http://example.com/stream1.m3u8",
				GroupId:    "group1",
				Attributes: map[string]string{"tvg-id": "ch1", "tvg-logo": "http://example.com/logo1.png"},
				Status:     200,
				IsOnline:   true,
				Tags:       []string{"hd"},
			},
			"id2": {
				Id:         "id2",
				Title:      "Channel Two",
				URL:        "http://example.com/stream2.m3u8",
				GroupId:    "group2",
				Attributes: map[string]string{"tvg-id": "ch2", "tvg-logo": "http://example.com/logo2.png"},
				Status:     404,
				IsOnline:   false,
				Tags:       []string{"untagged"},
			},
			"id3": {
				Id:         "id3",
				Title:      "Channel Three",
				URL:        "http://example.com/stream3.m3u8",
				GroupId:    "group1",
				Attributes: map[string]string{"tvg-id": "ch3", "tvg-logo": "http://example.com/logo3.png"},
				Status:     200,
				IsOnline:   true,
				Tags:       []string{"hd", "news"},
			},
		},
		OnlineCount:  2,
		OfflineCount: 1,
	}
}

// fetchBody читает тело HTTP-ответа целиком и возвращает его как строку
func fetchBody(t *testing.T, resp *http.Response) string {
	t.Helper()
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read body: %s", err)
	}
	return string(b)
}

// TestDetailsPageRendersChannelsTable проверяет, что на странице деталей
// онлайн-плейлиста присутствует таблица с каналами и данными для list.js
func TestDetailsPageRendersChannelsTable(t *testing.T) {
	ts, srv := makeTestServer(t)
	srv.putPlaylist(makeOnlinePlaylist())

	// Главная страница должна быть доступна
	resp, err := http.Get(ts.URL + "/")
	if err != nil {
		t.Fatalf("GET /: %s", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Errorf("GET /: expected 200, got %d", resp.StatusCode)
	}
	resp.Body.Close()

	// Страница деталей — основной сценарий
	resp, err = http.Get(ts.URL + "/test1/details")
	if err != nil {
		t.Fatalf("GET /test1/details: %s", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("GET /test1/details: expected 200, got %d", resp.StatusCode)
	}
	body := fetchBody(t, resp)

	checks := []struct {
		name string
		want bool
	}{
		// Заголовок и базовая структура
		{"заголовок 'Список каналов'", strings.Contains(body, "Список каналов")},
		{"id таблицы 'chlist'", strings.Contains(body, `id="chlist"`)},
		{"класс 'list' у tbody", strings.Contains(body, `class="list"`)},
		{"chcount = 3", strings.Contains(body, `<span id="chcount">3</span>`)},

		// Данные каналов в JSON
		{"values JSON", strings.Contains(body, "const values =")},
		{"Channel One в JSON", strings.Contains(body, "Channel One")},
		{"Channel Two в JSON", strings.Contains(body, "Channel Two")},
		{"Channel Three в JSON", strings.Contains(body, "Channel Three")},

		// Группы (отображаются когда > 1)
		{"id группы group1", strings.Contains(body, `value="group1"`)},
		{"id группы group2", strings.Contains(body, `value="group2"`)},

		// Теги
		{"тег #hd", strings.Contains(body, "btn-tag-hd")},
		{"тег #news", strings.Contains(body, "btn-tag-news")},
		{"тег #untagged", strings.Contains(body, "btn-tag-untagged")},

		// list.js
		{"list.js подключён", strings.Contains(body, "list.js@2.3.1")},
		{"getChannelTemplate функция", strings.Contains(body, "function getChannelTemplate")},
		{"опции list.js", strings.Contains(body, "const options =")},
		{"new List('chlist')", strings.Contains(body, "new List('chlist'")},
		{"getChannelTemplate возвращает <tr>", strings.Contains(body, "chrow")},

		// Счётчики
		{"OnlineCount отображается (~66%)", strings.Contains(body, "2 (66%)") || strings.Contains(body, "2 (67%)")},
		{"OfflineCount отображается (~33%)", strings.Contains(body, "1 (33%)") || strings.Contains(body, "1 (34%)")},
	}

	for _, c := range checks {
		if !c.want {
			t.Errorf("page missing: %s", c.name)
		}
	}
}

// TestDetailsPageOfflinePlaylist проверяет, что для оффлайн-плейлиста
// (IsKnown=true, IsOnline=false) вместо таблицы каналов показывается
// сообщение об ошибке
func TestDetailsPageOfflinePlaylist(t *testing.T) {
	ts, srv := makeTestServer(t)
	srv.putPlaylist(playlist.Playlist{
		Code:      "test1",
		Name:      "Test Playlist 1",
		Url:       "http://example.com/test1.m3u8",
		IsOnline:  false,
		Content:   "HTTP status 404",
		CheckedAt: 1700000000,
	})

	resp, err := http.Get(ts.URL + "/test1/details")
	if err != nil {
		t.Fatalf("GET /test1/details: %s", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	body := fetchBody(t, resp)

	if !strings.Contains(body, "Ошибка плейлиста") {
		t.Error("offline playlist should show error message")
	}
	if !strings.Contains(body, "HTTP status 404") {
		t.Error("offline playlist should show error text from Content")
	}
	if strings.Contains(body, `id="chlist"`) {
		t.Error("offline playlist should NOT render channels table")
	}
}

// TestDetailsPageUnknownPlaylist проверяет, что для непроверенного плейлиста
// (IsKnown=false) вместо таблицы каналов показывается предупреждение
func TestDetailsPageUnknownPlaylist(t *testing.T) {
	ts, _ := makeTestServer(t)

	resp, err := http.Get(ts.URL + "/test1/details")
	if err != nil {
		t.Fatalf("GET /test1/details: %s", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	body := fetchBody(t, resp)

	if !strings.Contains(body, "Список каналов сейчас неизвестен") {
		t.Error("unknown playlist should show 'list unknown' warning")
	}
	if strings.Contains(body, `id="chlist"`) {
		t.Error("unknown playlist should NOT render channels table")
	}
}

// TestDetailsPageNonexistentPlaylist проверяет, что несуществующий код
// возвращает 404
func TestDetailsPageNonexistentPlaylist(t *testing.T) {
	ts, _ := makeTestServer(t)

	resp, err := http.Get(ts.URL + "/nonexistent/details")
	if err != nil {
		t.Fatalf("GET /nonexistent/details: %s", err)
	}
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("expected 404, got %d", resp.StatusCode)
	}
	resp.Body.Close()
}
