// Package acceptance holds executable acceptance tests that mirror the Gherkin
// scenarios in docs/use-cases/. They drive the use-case engine through FAKE
// adapters (no live YouTube, no live brain) so the core behaviour is what's
// under test.
//
// These tests are intentionally RED in the scaffold: Engine.ProcessNewVideo
// returns usecase.ErrNotImplemented. Making them GREEN is the first build task.
package acceptance

import (
	"context"
	"testing"
	"time"

	"git.d-ma.be/mathias/tapir/internal/domain"
	"git.d-ma.be/mathias/tapir/internal/usecase"
)

// --- fake adapters ---------------------------------------------------------

type fakeSource struct {
	transcript domain.Transcript
}

func (f fakeSource) ListSubscriptions(ctx context.Context, userID string) ([]domain.Subscription, error) {
	return nil, nil
}
func (f fakeSource) NewVideos(ctx context.Context, sub domain.Subscription) ([]domain.Video, error) {
	return nil, nil
}
func (f fakeSource) FetchTranscript(ctx context.Context, v domain.Video) (domain.Transcript, error) {
	return f.transcript, nil
}

type fakeSummarizer struct{}

func (fakeSummarizer) Summarize(ctx context.Context, v domain.Video, t domain.Transcript) (domain.Summary, error) {
	return domain.Summary{
		VideoID:    v.ID,
		UserID:     v.UserID,
		Summary:    "a summary",
		Highlights: []string{"h1"},
		Takeaways:  []string{"t1"},
		AIProvider: "local",
		CreatedAt:  time.Now(),
	}, nil
}

type recordingSink struct {
	name      string
	delivered []domain.Summary
}

func (s *recordingSink) Name() string { return s.name }
func (s *recordingSink) Deliver(ctx context.Context, sum domain.Summary) error {
	s.delivered = append(s.delivered, sum)
	return nil
}

func sampleVideo() domain.Video {
	return domain.Video{
		ID: "v1", UserID: "u1", SubscriptionID: "s1",
		Provider: domain.ProviderYouTube, ProviderVideoID: "yt1",
		Title: "Designing for Attention", SeenAt: time.Now(),
	}
}

// --- scenarios -------------------------------------------------------------

// Scenario: A subscribed channel posts a video that has captions
func TestSubscribedVideoWithCaptionsIsSummarizedAndDelivered(t *testing.T) {
	src := fakeSource{transcript: domain.Transcript{
		VideoID: "v1", UserID: "u1", Source: domain.SourceCaptions,
		Language: "en", Content: "transcript text",
	}}
	sink := &recordingSink{name: "store"}
	eng := usecase.NewEngine(src, fakeSummarizer{}, sink)

	res, err := eng.ProcessNewVideo(context.Background(), sampleVideo())
	if err != nil {
		t.Fatalf("ProcessNewVideo returned error: %v", err)
	}
	if res.Skipped {
		t.Fatalf("expected video to be summarized, got skipped: %q", res.Reason)
	}
	if res.Summary == nil {
		t.Fatal("expected a summary, got nil")
	}
	if len(sink.delivered) != 1 {
		t.Fatalf("expected 1 delivery to store sink, got %d", len(sink.delivered))
	}
}

// Scenario: A subscribed channel posts a video with no usable transcript
func TestVideoWithNoTranscriptIsSkipped(t *testing.T) {
	src := fakeSource{transcript: domain.Transcript{
		VideoID: "v1", UserID: "u1", Source: domain.SourceNone,
	}}
	sink := &recordingSink{name: "store"}
	eng := usecase.NewEngine(src, fakeSummarizer{}, sink)

	res, err := eng.ProcessNewVideo(context.Background(), sampleVideo())
	if err != nil {
		t.Fatalf("ProcessNewVideo returned error: %v", err)
	}
	if !res.Skipped {
		t.Fatal("expected video to be skipped (no transcript)")
	}
	if res.Reason != "no transcript" {
		t.Fatalf("expected reason %q, got %q", "no transcript", res.Reason)
	}
	if res.Summary != nil {
		t.Fatal("expected no summary for skipped video")
	}
	if len(sink.delivered) != 0 {
		t.Fatalf("expected no summary delivery, got %d", len(sink.delivered))
	}
}

// --- watch-loop fakes ------------------------------------------------------

// watchSource models a provider that knows about videos on several channels but
// only surfaces those on channels the user is subscribed to. NewVideos returns
// the (possibly repeated) videos for a subscription each time it is polled.
type watchSource struct {
	subs         []domain.Subscription
	videosBySub  map[string][]domain.Video
	transcripts  map[string]domain.Transcript // keyed by video ID
	newVideoHits int
}

func (s *watchSource) ListSubscriptions(ctx context.Context, userID string) ([]domain.Subscription, error) {
	return s.subs, nil
}
func (s *watchSource) NewVideos(ctx context.Context, sub domain.Subscription) ([]domain.Video, error) {
	s.newVideoHits++
	return s.videosBySub[sub.ID], nil
}
func (s *watchSource) FetchTranscript(ctx context.Context, v domain.Video) (domain.Transcript, error) {
	return s.transcripts[v.ID], nil
}

// countingSummarizer records how many times Summarize is invoked.
type countingSummarizer struct{ calls int }

func (c *countingSummarizer) Summarize(ctx context.Context, v domain.Video, t domain.Transcript) (domain.Summary, error) {
	c.calls++
	return domain.Summary{VideoID: v.ID, UserID: v.UserID, Summary: "a summary", AIProvider: "local", CreatedAt: time.Now()}, nil
}

// Scenario: A channel I am not subscribed to posts a video
func TestUnsubscribedChannelVideoIsNotProcessed(t *testing.T) {
	subscribed := domain.Subscription{ID: "s1", UserID: "u1", ChannelID: "acme", ChannelTitle: "Acme Talks", Active: true}
	acme := domain.Video{ID: "v1", UserID: "u1", SubscriptionID: "s1", Provider: domain.ProviderYouTube, ProviderVideoID: "yt1", Title: "Designing for Attention", SeenAt: time.Now()}
	// "Random Co" video exists in the wider world but the user is not subscribed,
	// so its subscription is absent and NewVideos never surfaces it.
	src := &watchSource{
		subs:        []domain.Subscription{subscribed},
		videosBySub: map[string][]domain.Video{"s1": {acme}},
		transcripts: map[string]domain.Transcript{"v1": {VideoID: "v1", UserID: "u1", Source: domain.SourceCaptions, Language: "en", Content: "transcript text"}},
	}
	sum := &countingSummarizer{}
	sink := &recordingSink{name: "store"}
	eng := usecase.NewEngine(src, sum, sink)

	results, err := eng.ProcessNewVideos(context.Background(), "u1")
	if err != nil {
		t.Fatalf("ProcessNewVideos returned error: %v", err)
	}
	if len(results) != 1 {
		t.Fatalf("expected exactly 1 video processed (the subscribed one), got %d", len(results))
	}
	if results[0].Video.ID != "v1" {
		t.Fatalf("expected the subscribed video v1 to be processed, got %q", results[0].Video.ID)
	}
	for _, d := range sink.delivered {
		if d.VideoID == "unrelated" {
			t.Fatal("unsubscribed channel's video was processed and delivered")
		}
	}
}

// Scenario: The same video is not summarized twice
func TestAlreadySummarizedVideoIsNotReprocessed(t *testing.T) {
	subscribed := domain.Subscription{ID: "s1", UserID: "u1", ChannelID: "acme", ChannelTitle: "Acme Talks", Active: true}
	video := domain.Video{ID: "v1", UserID: "u1", SubscriptionID: "s1", Provider: domain.ProviderYouTube, ProviderVideoID: "yt1", Title: "Designing for Attention", SeenAt: time.Now()}
	src := &watchSource{
		subs:        []domain.Subscription{subscribed},
		videosBySub: map[string][]domain.Video{"s1": {video}},
		transcripts: map[string]domain.Transcript{"v1": {VideoID: "v1", UserID: "u1", Source: domain.SourceCaptions, Language: "en", Content: "transcript text"}},
	}
	sum := &countingSummarizer{}
	sink := &recordingSink{name: "store"}
	eng := usecase.NewEngine(src, sum, sink)

	// First watch pass summarizes the video.
	if _, err := eng.ProcessNewVideos(context.Background(), "u1"); err != nil {
		t.Fatalf("first pass returned error: %v", err)
	}
	// The watcher sees the same video again on the next pass.
	if _, err := eng.ProcessNewVideos(context.Background(), "u1"); err != nil {
		t.Fatalf("second pass returned error: %v", err)
	}

	if sum.calls != 1 {
		t.Fatalf("expected the video to be summarized exactly once, got %d calls", sum.calls)
	}
	if len(sink.delivered) != 1 {
		t.Fatalf("expected exactly 1 delivery, got %d", len(sink.delivered))
	}
}
