package photos

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	photospkg "github.com/ultisuite/ulti-backend/internal/photos"
)

func sampleAssets() []photospkg.Asset {
	return []photospkg.Asset{
		{ID: "a1", OriginalName: "alpha.jpg", MimeType: "image/jpeg", FileSize: 100, CreatedAt: "2026-01-01T10:00:00Z"},
		{ID: "a2", OriginalName: "beta.png", MimeType: "image/png", FileSize: 300, CreatedAt: "2026-01-15T10:00:00Z"},
		{ID: "a3", OriginalName: "gamma.jpg", MimeType: "image/jpeg", FileSize: 200, CreatedAt: "2026-02-01T10:00:00Z"},
		{ID: "a4", OriginalName: "delta.tiff", MimeType: "image/tiff", FileSize: 50, CreatedAt: "2026-02-10T10:00:00Z"},
	}
}

func sampleAlbums() []photospkg.Album {
	return []photospkg.Album{
		{ID: "al1", Name: "Alpha Album", AssetCount: 5, CreatedAt: "2026-01-01T10:00:00Z"},
		{ID: "al2", Name: "Beta Album", AssetCount: 15, CreatedAt: "2026-01-20T10:00:00Z"},
		{ID: "al3", Name: "Gamma Album", AssetCount: 8, CreatedAt: "2026-02-05T10:00:00Z"},
	}
}

func mustDate(t *testing.T, raw string) *time.Time {
	t.Helper()
	d, err := query.ParseDate(raw)
	if err != nil {
		t.Fatalf("ParseDate(%q): %v", raw, err)
	}
	return &d
}

func mustEndDate(t *testing.T, raw string) *time.Time {
	t.Helper()
	d, err := query.ParseDate(raw)
	if err != nil {
		t.Fatalf("ParseDate(%q): %v", raw, err)
	}
	end := d.Add(24*time.Hour - time.Nanosecond)
	return &end
}

func TestApplyAssetListPagination(t *testing.T) {
	params := query.ListParams{Page: 2, PageSize: 2, Sort: "name"}
	page, total := applyAssetList(sampleAssets(), params)
	if total != 4 {
		t.Fatalf("total = %d, want 4", total)
	}
	if len(page) != 2 {
		t.Fatalf("page len = %d, want 2", len(page))
	}
	if page[0].ID != "a4" || page[1].ID != "a3" {
		t.Fatalf("page ids = %q/%q, want a4/a3", page[0].ID, page[1].ID)
	}
	meta := params.Meta(&total)
	if meta.Page != 2 || meta.PageSize != 2 || meta.Total == nil || *meta.Total != 4 {
		t.Fatalf("meta = %+v", meta)
	}
}

func TestApplyAssetListPaginationBeyondRange(t *testing.T) {
	params := query.ListParams{Page: 10, PageSize: 2}
	page, total := applyAssetList(sampleAssets(), params)
	if total != 4 {
		t.Fatalf("total = %d, want 4", total)
	}
	if len(page) != 0 {
		t.Fatalf("page len = %d, want 0", len(page))
	}
}

func TestApplyAssetListSortByNameAsc(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Sort: "name"}
	page, _ := applyAssetList(sampleAssets(), params)
	if len(page) != 4 {
		t.Fatalf("len = %d", len(page))
	}
	want := []string{"a1", "a2", "a4", "a3"}
	for i, id := range want {
		if page[i].ID != id {
			t.Fatalf("page[%d].ID = %q, want %q", i, page[i].ID, id)
		}
	}
}

func TestApplyAssetListSortBySizeDesc(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Sort: "-size"}
	page, _ := applyAssetList(sampleAssets(), params)
	want := []string{"a2", "a3", "a1", "a4"}
	for i, id := range want {
		if page[i].ID != id {
			t.Fatalf("page[%d].ID = %q, want %q", i, page[i].ID, id)
		}
	}
}

func TestApplyAssetListSortByCreatedAtDefaultDesc(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10}
	page, _ := applyAssetList(sampleAssets(), params)
	want := []string{"a4", "a3", "a2", "a1"}
	for i, id := range want {
		if page[i].ID != id {
			t.Fatalf("page[%d].ID = %q, want %q", i, page[i].ID, id)
		}
	}
}

func TestApplyAssetListFilterByQ(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Q: "jpg"}
	page, total := applyAssetList(sampleAssets(), params)
	if total != 2 {
		t.Fatalf("total = %d, want 2", total)
	}
	if page[0].ID != "a3" || page[1].ID != "a1" {
		t.Fatalf("ids = %q/%q", page[0].ID, page[1].ID)
	}
}

func TestApplyAssetListFilterByQMimeType(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Q: "tiff"}
	page, total := applyAssetList(sampleAssets(), params)
	if total != 1 || page[0].ID != "a4" {
		t.Fatalf("total=%d page=%+v", total, page)
	}
}

func TestApplyAssetListFilterByDateRange(t *testing.T) {
	params := query.ListParams{
		Page:     1,
		PageSize: 10,
		From:     mustDate(t, "2026-01-10"),
		To:       mustEndDate(t, "2026-02-05"),
	}
	page, total := applyAssetList(sampleAssets(), params)
	if total != 2 {
		t.Fatalf("total = %d, want 2", total)
	}
	if page[0].ID != "a3" || page[1].ID != "a2" {
		t.Fatalf("ids = %q/%q", page[0].ID, page[1].ID)
	}
}

func TestApplyAlbumListPagination(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 2, Sort: "name"}
	page, total := applyAlbumList(sampleAlbums(), params)
	if total != 3 || len(page) != 2 {
		t.Fatalf("total=%d len=%d", total, len(page))
	}
	if page[0].ID != "al1" || page[1].ID != "al2" {
		t.Fatalf("ids = %q/%q", page[0].ID, page[1].ID)
	}
}

func TestApplyAlbumListSortBySizeAsc(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Sort: "size"}
	page, _ := applyAlbumList(sampleAlbums(), params)
	want := []string{"al1", "al3", "al2"}
	for i, id := range want {
		if page[i].ID != id {
			t.Fatalf("page[%d].ID = %q, want %q", i, page[i].ID, id)
		}
	}
}

func TestApplyAlbumListFilterByQAndSortCreatedAtDesc(t *testing.T) {
	params := query.ListParams{Page: 1, PageSize: 10, Q: "album", Sort: "-created_at"}
	page, total := applyAlbumList(sampleAlbums(), params)
	if total != 3 {
		t.Fatalf("total = %d, want 3", total)
	}
	if page[0].ID != "al3" {
		t.Fatalf("first id = %q, want al3", page[0].ID)
	}
}

func TestApplyAlbumListFilterByFromDate(t *testing.T) {
	params := query.ListParams{
		Page:     1,
		PageSize: 10,
		From:     mustDate(t, "2026-01-15"),
		Sort:     "name",
	}
	page, total := applyAlbumList(sampleAlbums(), params)
	if total != 2 {
		t.Fatalf("total = %d, want 2", total)
	}
	if page[0].ID != "al2" || page[1].ID != "al3" {
		t.Fatalf("ids = %q/%q", page[0].ID, page[1].ID)
	}
}

func TestServiceListAssetsUsesPaginationMeta(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/assets" {
			http.NotFound(w, r)
			return
		}
		_ = json.NewEncoder(w).Encode(sampleAssets())
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	result, err := svc.ListAssets(context.Background(), "key", query.ListParams{
		Page: 2, PageSize: 2, Sort: "name",
	})
	if err != nil {
		t.Fatalf("ListAssets: %v", err)
	}
	if len(result.Assets) != 2 || result.Page != 2 {
		t.Fatalf("assets=%d page=%d", len(result.Assets), result.Page)
	}
	if result.Pagination.Page != 2 || result.Pagination.PageSize != 2 {
		t.Fatalf("pagination = %+v", result.Pagination)
	}
	if result.Pagination.Total == nil || *result.Pagination.Total != 4 {
		t.Fatalf("total = %v", result.Pagination.Total)
	}
}

func TestServiceListAlbumsUsesPaginationMeta(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/albums" {
			http.NotFound(w, r)
			return
		}
		_ = json.NewEncoder(w).Encode(sampleAlbums())
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	result, err := svc.ListAlbums(context.Background(), "key", query.ListParams{
		Page: 1, PageSize: 1, Sort: "-size",
	})
	if err != nil {
		t.Fatalf("ListAlbums: %v", err)
	}
	if len(result.Albums) != 1 || result.Albums[0].ID != "al2" {
		t.Fatalf("albums = %+v", result.Albums)
	}
	if result.Pagination.Total == nil || *result.Pagination.Total != 3 {
		t.Fatalf("total = %v", result.Pagination.Total)
	}
}

func TestServiceGetAlbumNotFound(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	_, err := svc.GetAlbum(context.Background(), "key", "missing")
	if err == nil || !errors.Is(err, ErrNotFound) {
		t.Fatalf("GetAlbum err = %v, want ErrNotFound", err)
	}
}

func TestServiceCreateAlbum(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/albums" {
			http.NotFound(w, r)
			return
		}
		w.WriteHeader(http.StatusCreated)
		_ = json.NewEncoder(w).Encode(photospkg.Album{ID: "new-al", Name: "Created", AssetCount: 1})
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	album, err := svc.CreateAlbum(context.Background(), "key", &createAlbumRequest{
		Name:     "Created",
		AssetIDs: []string{"a1"},
	})
	if err != nil {
		t.Fatalf("CreateAlbum: %v", err)
	}
	if album.ID != "new-al" || album.Name != "Created" {
		t.Fatalf("album = %+v", album)
	}
}

func TestServiceUpdateAlbumInvalid(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	_, err := svc.UpdateAlbum(context.Background(), "key", "al1", &updateAlbumRequest{Name: strPtr("Renamed")})
	if err == nil || !errors.Is(err, ErrInvalid) {
		t.Fatalf("UpdateAlbum err = %v, want ErrInvalid", err)
	}
}

func TestServiceDeleteAlbumNotFound(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	err := svc.DeleteAlbum(context.Background(), "key", "missing")
	if err == nil || !errors.Is(err, ErrNotFound) {
		t.Fatalf("DeleteAlbum err = %v, want ErrNotFound", err)
	}
}

func TestServiceAddAlbumAssets(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut || r.URL.Path != "/albums/al1/assets" {
			http.NotFound(w, r)
			return
		}
		_ = json.NewEncoder(w).Encode([]photospkg.BulkAssetResult{{ID: "a1", Success: true}})
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), nil)
	result, err := svc.AddAlbumAssets(context.Background(), "key", "al1", []string{"a1"})
	if err != nil {
		t.Fatalf("AddAlbumAssets: %v", err)
	}
	if len(result.Results) != 1 || !result.Results[0].Success {
		t.Fatalf("results = %+v", result.Results)
	}
}

func strPtr(value string) *string {
	return &value
}

type quotaStub struct {
	quota nextcloud.UserQuota
	err   error
}

func (q quotaStub) GetQuota(context.Context, string) (nextcloud.UserQuota, error) {
	return q.quota, q.err
}

func TestServiceUploadAssetQuotaExceeded(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("upload should not be called when quota exceeded")
	}))
	defer server.Close()

	svc := NewService(photospkg.NewClient(server.URL), quotaStub{
		quota: nextcloud.UserQuota{Free: 10},
	})

	ctx := context.WithValue(context.Background(), contentLengthContextKey{}, int64(100))
	_, err := svc.UploadAsset(ctx, "user-1", strings.NewReader("payload"), "application/octet-stream")
	if err == nil || !errors.Is(err, ErrQuotaExceeded) {
		t.Fatalf("UploadAsset err = %v, want ErrQuotaExceeded", err)
	}
}
