//go:build test

package main

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"github.com/einand/applephotos/internal/photos"
)

type mockBridge struct {
	accessErr      error
	albums         []photos.Album
	albumsErr      error
	assets         []photos.Asset
	assetsErr      error
	assetsByAlbum  map[string][]photos.Asset
	tree           []photos.CollectionNode
	treeErr        error
	exportN        int
	exportErr      error
	exportOrigN    int
	exportOrigErr  error
	exportPreviewFn func(string, string, int) (int, error)
	exportOrigFn    func(string, string) (int, error)
	backupAllN      int
	backupAllErr    error
	backupAllFn     func(string, int, bool) (int, error)
	cancelled       bool
	exportPreviewFn2 func(string, string, int, int) (photos.ExportResult, error)
	exportOrigFn2    func(string, string, int) (photos.ExportResult, error)
}

func (m *mockBridge) RequestAccess() error { return m.accessErr }
func (m *mockBridge) ListAlbums() ([]photos.Album, error) { return m.albums, m.albumsErr }
func (m *mockBridge) ListAssets(albumID string) ([]photos.Asset, int, error) {
	if m.assetsByAlbum != nil {
		if assets, ok := m.assetsByAlbum[albumID]; ok {
			return assets, len(assets), nil
		}
		return nil, 0, fmt.Errorf("album not found")
	}
	return m.assets, len(m.assets), m.assetsErr
}
func (m *mockBridge) ListTree() ([]photos.CollectionNode, error) { return m.tree, m.treeErr }
func (m *mockBridge) ExportAlbumPreviews(albumID, out string, size int) (int, error) {
	if m.exportPreviewFn != nil {
		return m.exportPreviewFn(albumID, out, size)
	}
	return m.exportN, m.exportErr
}
func (m *mockBridge) ExportAlbumOriginals(albumID, out string) (int, error) {
	if m.exportOrigFn != nil {
		return m.exportOrigFn(albumID, out)
	}
	return m.exportOrigN, m.exportOrigErr
}
func (m *mockBridge) ExportPreview(assetID, out string, targetSize, index int) (photos.ExportResult, error) {
	if m.exportPreviewFn2 != nil {
		return m.exportPreviewFn2(assetID, out, targetSize, index)
	}
	return photos.ExportResult{Filename: "0000_test.jpg", Size: 1024, Cloud: "local"}, nil
}
func (m *mockBridge) ExportOriginal(assetID, out string, index int) (photos.ExportResult, error) {
	if m.exportOrigFn2 != nil {
		return m.exportOrigFn2(assetID, out, index)
	}
	return photos.ExportResult{Filename: "test.jpg", Size: 2048, Cloud: "cloud"}, nil
}
func (m *mockBridge) BackupAll(out string, size int, originals bool) (int, error) {
	if m.backupAllFn != nil {
		return m.backupAllFn(out, size, originals)
	}
	return m.backupAllN, m.backupAllErr
}
func (m *mockBridge) Cancel() { m.cancelled = true }

func runWith(args []string, b photos.Bridge) (string, string, int) {
	var out, err bytes.Buffer
	rc := run(args, &out, &err, b)
	return out.String(), err.String(), rc
}

func TestRunNoArgs(t *testing.T) {
	_, stderr, rc := runWith(nil, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "applephotos") {
		t.Errorf("stderr should contain usage, got: %s", stderr)
	}
}

func TestRunHelp(t *testing.T) {
	for _, cmd := range []string{"help", "--help", "-h"} {
		_, stderr, rc := runWith([]string{cmd}, &mockBridge{})
		if rc != 0 {
			t.Errorf("%s: rc = %d, want 0", cmd, rc)
		}
		if !strings.Contains(stderr, "applephotos") {
			t.Errorf("%s: stderr should contain usage", cmd)
		}
	}
}

func TestRunUnknownCommand(t *testing.T) {
	_, stderr, rc := runWith([]string{"foo"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "unknown command: foo") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdAlbumsSuccess(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{{ID: "a1", Title: "Vacation"}, {ID: "a2", Title: "Work"}},
	}
	out, _, rc := runWith([]string{"albums"}, b)
	if rc != 0 {
		t.Errorf("rc = %d", rc)
	}
	expected := "a1\tVacation\na2\tWork\n"
	if out != expected {
		t.Errorf("out = %q, want %q", out, expected)
	}
}

func TestCmdAlbumsAuthDenied(t *testing.T) {
	b := &mockBridge{accessErr: fmt.Errorf("denied")}
	_, stderr, rc := runWith([]string{"albums"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "denied") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdAlbumsBridgeError(t *testing.T) {
	b := &mockBridge{albumsErr: fmt.Errorf("boom")}
	_, stderr, rc := runWith([]string{"albums"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "boom") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdAlbumsEmpty(t *testing.T) {
	b := &mockBridge{albums: []photos.Album{}}
	out, _, rc := runWith([]string{"albums"}, b)
	if rc != 0 {
		t.Errorf("rc = %d", rc)
	}
	if out != "" {
		t.Errorf("out = %q, want empty", out)
	}
}

func TestCmdPhotosMissingAlbumID(t *testing.T) {
	_, stderr, rc := runWith([]string{"photos"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "--album-id is required") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdPhotosSuccess(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "as1", Filename: "IMG_0001.JPG"}, {ID: "as2", Filename: "IMG_0002.JPG", Cloud: true}},
	}
	out, _, rc := runWith([]string{"photos", "--album-id", "x"}, b)
	if rc != 0 {
		t.Errorf("rc = %d", rc)
	}
	expected := "as1\tIMG_0001.JPG\tlocal\nas2\tIMG_0002.JPG\tcloud\n"
	if out != expected {
		t.Errorf("out = %q, want %q", out, expected)
	}
}

func TestCmdPhotosAuthDenied(t *testing.T) {
	b := &mockBridge{accessErr: fmt.Errorf("nope")}
	_, stderr, rc := runWith([]string{"photos", "--album-id", "x"}, b)
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "nope") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdPhotosBridgeError(t *testing.T) {
	b := &mockBridge{assetsErr: fmt.Errorf("fail")}
	_, stderr, rc := runWith([]string{"photos", "--album-id", "x"}, b)
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "fail") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdTreeSuccess(t *testing.T) {
	b := &mockBridge{tree: []photos.CollectionNode{
		{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{Name: "Italy 2024", Kind: "folder", Children: []photos.CollectionNode{{Name: "Venice", Kind: "album"}}}}},
		{Name: "Favorites", Kind: "album"},
	}}
	out, _, rc := runWith([]string{"tree"}, b)
	if rc != 0 {
		t.Errorf("rc = %d", rc)
	}
	expected := "Trips\n  Italy 2024\n    Venice\nFavorites\n"
	if out != expected {
		t.Errorf("out = %q, want %q", out, expected)
	}
}

func TestCmdTreeAuthDenied(t *testing.T) {
	b := &mockBridge{accessErr: fmt.Errorf("denied")}
	_, stderr, rc := runWith([]string{"tree"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "denied") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdTreeBridgeError(t *testing.T) {
	b := &mockBridge{treeErr: fmt.Errorf("boom")}
	_, stderr, rc := runWith([]string{"tree"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, want 1", rc)
	}
	if !strings.Contains(stderr, "boom") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllPreviewSuccess(t *testing.T) {
	b := &mockBridge{
		tree: []photos.CollectionNode{{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}}}, {ID: "a2", Name: "Favorites", Kind: "album"}},
		assetsByAlbum: map[string][]photos.Asset{
			"a1": {{ID: "as1", Filename: "img1.jpg"}},
			"a2": {{ID: "as2", Filename: "img2.jpg"}},
		},
	}
	_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--size", "2048"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 2 preview files across 2 albums to /backup") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllOriginalsSuccess(t *testing.T) {
	b := &mockBridge{
		tree: []photos.CollectionNode{{Name: "Trips", Kind: "folder", Children: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}}}},
		assetsByAlbum: map[string][]photos.Asset{
			"a1": {{ID: "as1", Filename: "img.jpg"}, {ID: "as2", Filename: "img2.jpg"}, {ID: "as3", Filename: "img3.jpg"}},
		},
	}
	_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--originals", "--size", "bad"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 3 original files across 1 albums to /backup") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllMissingOutDir(t *testing.T) {
	_, stderr, rc := runWith([]string{"backup-all"}, &mockBridge{})
	if rc != 1 {
		t.Fatalf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--out is required") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllInvalidSize(t *testing.T) {
	b := &mockBridge{tree: []photos.CollectionNode{}}
	_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup", "--size", "bad"}, b)
	if rc != 1 {
		t.Fatalf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--size must be a positive integer") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllTreeError(t *testing.T) {
	b := &mockBridge{treeErr: fmt.Errorf("tree fail")}
	_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup"}, b)
	if rc != 1 {
		t.Fatalf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "tree fail") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdBackupAllExportError(t *testing.T) {
	b := &mockBridge{
		tree: []photos.CollectionNode{{ID: "a1", Name: "Venice", Kind: "album"}},
		assetsByAlbum: map[string][]photos.Asset{
			"a1": {{ID: "as1", Filename: "img.jpg"}},
		},
		exportPreviewFn2: func(string, string, int, int) (photos.ExportResult, error) {
			return photos.ExportResult{}, fmt.Errorf("disk full")
		},
	}
	_, stderr, rc := runWith([]string{"backup-all", "--out", "/backup"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	_ = stderr
}

func TestCmdExportMissingAlbumID(t *testing.T) {
	_, stderr, rc := runWith([]string{"export", "--out", "/tmp"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--album-id is required") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportMissingOutDir(t *testing.T) {
	_, stderr, rc := runWith([]string{"export", "--album-id", "x"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--out is required") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportInvalidSize(t *testing.T) {
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp", "--size", "abc"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--size must be a positive integer") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportNegativeSize(t *testing.T) {
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp", "--size", "-5"}, &mockBridge{})
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "--size must be a positive integer") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportSuccess(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out"}, b)
	if rc != 0 {
		t.Errorf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 1 photos to /tmp/out") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportDefaultSize(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
	if rc != 0 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "exported 1") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportAuthDenied(t *testing.T) {
	b := &mockBridge{accessErr: fmt.Errorf("no")}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
	if rc != 1 {
		t.Errorf("rc = %d", rc)
	}
	if !strings.Contains(stderr, "no") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportBridgeError(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
		exportPreviewFn2: func(string, string, int, int) (photos.ExportResult, error) {
			return photos.ExportResult{}, fmt.Errorf("disk full")
		},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "all exports failed") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportOriginalsSuccess(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals"}, b)
	if rc != 0 {
		t.Errorf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 1 original files to /tmp/out") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportOriginalsIgnoresSizeValidation(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals", "--size", "abc"}, b)
	if rc != 0 {
		t.Errorf("rc = %d, stderr = %q", rc, stderr)
	}
	if strings.Contains(stderr, "--size must be a positive integer") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportOriginalsBridgeError(t *testing.T) {
	b := &mockBridge{
		assets: []photos.Asset{{ID: "a1", Filename: "img.jpg"}},
		exportOrigFn2: func(string, string, int) (photos.ExportResult, error) {
			return photos.ExportResult{}, fmt.Errorf("copy failed")
		},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "x", "--out", "/tmp/out", "--originals"}, b)
	if rc != 1 {
		t.Errorf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "all exports failed") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestFlagVal(t *testing.T) {
	args := []string{"--album-id", "abc", "--out", "/tmp"}
	if v := flagVal(args, "--album-id"); v != "abc" {
		t.Errorf("got %q", v)
	}
	if v := flagVal(args, "--out"); v != "/tmp" {
		t.Errorf("got %q", v)
	}
	if v := flagVal(args, "--missing"); v != "" {
		t.Errorf("got %q, want empty", v)
	}
}

func TestFlagValTrailing(t *testing.T) {
	args := []string{"--album-id"}
	if v := flagVal(args, "--album-id"); v != "" {
		t.Errorf("got %q, want empty for trailing flag", v)
	}
}

func TestFlagValWithDefault(t *testing.T) {
	args := []string{"--size", "2048"}
	if v := flagValWithDefault(args, "--size", "1024"); v != "2048" {
		t.Errorf("got %q", v)
	}
	if v := flagValWithDefault(args, "--missing", "fallback"); v != "fallback" {
		t.Errorf("got %q", v)
	}
}

func TestHasFlag(t *testing.T) {
	args := []string{"export", "--originals", "--album-id", "x"}
	if !hasFlag(args, "--originals") {
		t.Fatal("expected --originals to be found")
	}
	if hasFlag(args, "--missing") {
		t.Fatal("did not expect missing flag")
	}
}

func TestFlagValEmptyArgs(t *testing.T) {
	if v := flagVal(nil, "--album-id"); v != "" {
		t.Errorf("got %q", v)
	}
	if v := flagValWithDefault(nil, "--size", "1024"); v != "1024" {
		t.Errorf("got %q", v)
	}
}

func TestResolveAlbumIDDirectMatch(t *testing.T) {
	b := &mockBridge{
		assetsByAlbum: map[string][]photos.Asset{
			"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}},
		},
	}
	id, err := resolveAlbumID(b, "ABC/L0/001")
	if err != nil {
		t.Fatal(err)
	}
	if id != "ABC/L0/001" {
		t.Errorf("got %q", id)
	}
}

func TestResolveAlbumIDByName(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{
			{ID: "ABC/L0/001", Title: "DnD"},
			{ID: "DEF/L0/001", Title: "Work"},
		},
		assetsByAlbum: map[string][]photos.Asset{
			"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}},
		},
	}
	id, err := resolveAlbumID(b, "DnD")
	if err != nil {
		t.Fatal(err)
	}
	if id != "ABC/L0/001" {
		t.Errorf("got %q, want ABC/L0/001", id)
	}
}

func TestResolveAlbumIDNotFound(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{
			{ID: "ABC/L0/001", Title: "DnD"},
		},
		assetsByAlbum: map[string][]photos.Asset{},
	}
	_, err := resolveAlbumID(b, "Nonexistent")
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestResolveAlbumIDListAlbumsFails(t *testing.T) {
	b := &mockBridge{
		albumsErr: fmt.Errorf("no access"),
		assetsByAlbum: map[string][]photos.Asset{},
	}
	_, err := resolveAlbumID(b, "DnD")
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestCmdPhotosResolvesAlbumName(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{
			{ID: "ABC/L0/001", Title: "DnD"},
		},
		assetsByAlbum: map[string][]photos.Asset{
			"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}},
		},
	}
	out, stderr, rc := runWith([]string{"photos", "--album-id", "DnD"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(out, "a1\timg.jpg\tlocal") {
		t.Errorf("out = %q", out)
	}
}

func TestCmdExportResolvesAlbumName(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{
			{ID: "ABC/L0/001", Title: "DnD"},
		},
		assetsByAlbum: map[string][]photos.Asset{
			"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}, {ID: "a2", Filename: "img2.jpg"}, {ID: "a3", Filename: "img3.jpg"}, {ID: "a4", Filename: "img4.jpg"}, {ID: "a5", Filename: "img5.jpg"}},
		},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "DnD", "--out", "/tmp/out"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 5 photos") {
		t.Errorf("stderr = %q", stderr)
	}
}

func TestCmdExportOriginalsResolvesAlbumName(t *testing.T) {
	b := &mockBridge{
		albums: []photos.Album{
			{ID: "ABC/L0/001", Title: "DnD"},
		},
		assetsByAlbum: map[string][]photos.Asset{
			"ABC/L0/001": {{ID: "a1", Filename: "img.jpg"}, {ID: "a2", Filename: "img2.jpg"}, {ID: "a3", Filename: "img3.jpg"}},
		},
	}
	_, stderr, rc := runWith([]string{"export", "--album-id", "DnD", "--out", "/tmp/out", "--originals"}, b)
	if rc != 0 {
		t.Fatalf("rc = %d, stderr = %q", rc, stderr)
	}
	if !strings.Contains(stderr, "exported 3 original files") {
		t.Errorf("stderr = %q", stderr)
	}
}
