package factory

import (
	"bytes"
	"context"
	"io"
	"testing"
	"time"

	"aoiprxkit"
	"github.com/jan/fm-rds-tx/internal/audio"
	"github.com/jan/fm-rds-tx/internal/config"
	"github.com/jan/fm-rds-tx/internal/ingest"
	aoipad "github.com/jan/fm-rds-tx/internal/ingest/adapters/aoip"
)

type streamReadCloser struct{ io.Reader }

func (r streamReadCloser) Close() error { return nil }

type stubAES67Receiver struct {
	onStart func()
}

func (r *stubAES67Receiver) Start(context.Context) error {
	if r.onStart != nil {
		r.onStart()
	}
	return nil
}

func (r *stubAES67Receiver) Stop() error { return nil }
func (r *stubAES67Receiver) Stats() aoiprxkit.Stats {
	return aoiprxkit.Stats{}
}

func TestHTTPRawFactoryToRuntimeSmoke(t *testing.T) {
	cfg := config.Default()
	cfg.Ingest.Kind = "http-raw"
	cfg.Ingest.HTTPRaw.SampleRateHz = 44100
	cfg.Ingest.HTTPRaw.Channels = 2

	src, ingress, err := BuildSource(cfg, Deps{})
	if err != nil {
		t.Fatalf("build source: %v", err)
	}
	if src == nil || ingress == nil {
		t.Fatalf("expected source and ingress for kind=http-raw")
	}

	sink := audio.NewStreamSource(128, cfg.Ingest.HTTPRaw.SampleRateHz)
	rt := ingest.NewRuntime(sink, src)
	if err := rt.Start(context.Background()); err != nil {
		t.Fatalf("runtime start: %v", err)
	}
	defer rt.Stop()

	// Two stereo frames: L1,R1,L2,R2 (S16LE).
	frames, err := ingress.WritePCM16([]byte{
		0xE8, 0x03, 0x18, 0xFC,
		0xD0, 0x07, 0x30, 0xF8,
	})
	if err != nil {
		t.Fatalf("write pcm16: %v", err)
	}
	if frames != 2 {
		t.Fatalf("frames=%d want 2", frames)
	}

	waitForSinkFrames(t, sink, 2)

	stats := rt.Stats()
	if stats.Active.Kind != "http-raw" {
		t.Fatalf("active kind=%q want http-raw", stats.Active.Kind)
	}
	if stats.Source.ChunksIn != 1 {
		t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
	}
	if stats.Source.SamplesIn != 4 {
		t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
	}
	if stats.Runtime.State != "running" {
		t.Fatalf("runtime state=%q want running", stats.Runtime.State)
	}
	if stats.Runtime.LastChunkAt.IsZero() {
		t.Fatalf("runtime lastChunkAt should be set")
	}
}

func TestSRTFactoryToRuntimeSmoke(t *testing.T) {
	var stream bytes.Buffer
	if err := aoiprxkit.WritePCM32Packet(&stream, 2, 48000, 2, 1, 480, []int32{11, -11, 22, -22}); err != nil {
		t.Fatalf("write packet: %v", err)
	}

	cfg := config.Default()
	cfg.Ingest.Kind = "srt"
	cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000?mode=listener"
	cfg.Ingest.SRT.SampleRateHz = 48000
	cfg.Ingest.SRT.Channels = 2

	src, ingress, err := BuildSource(cfg, Deps{
		SRTOpener: func(ctx context.Context, srtCfg aoiprxkit.SRTConfig) (io.ReadCloser, error) {
			_ = ctx
			_ = srtCfg
			return streamReadCloser{Reader: bytes.NewReader(stream.Bytes())}, nil
		},
	})
	if err != nil {
		t.Fatalf("build source: %v", err)
	}
	if src == nil {
		t.Fatalf("expected source for kind=srt")
	}
	if ingress != nil {
		t.Fatalf("expected no ingress for kind=srt")
	}

	sink := audio.NewStreamSource(128, cfg.Ingest.SRT.SampleRateHz)
	rt := ingest.NewRuntime(sink, src)
	if err := rt.Start(context.Background()); err != nil {
		t.Fatalf("runtime start: %v", err)
	}
	defer rt.Stop()

	waitForSinkFrames(t, sink, 2)

	stats := rt.Stats()
	if stats.Active.Kind != "srt" {
		t.Fatalf("active kind=%q want srt", stats.Active.Kind)
	}
	if stats.Source.ChunksIn != 1 {
		t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
	}
	if stats.Source.SamplesIn != 4 {
		t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
	}
}

func TestAES67FactoryToRuntimeSmoke(t *testing.T) {
	cfg := config.Default()
	cfg.Ingest.Kind = "aes67"
	cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
	cfg.Ingest.AES67.Port = 5004
	cfg.Ingest.AES67.SampleRateHz = 48000
	cfg.Ingest.AES67.Channels = 2
	cfg.Ingest.AES67.Encoding = "L24"
	cfg.Ingest.AES67.PacketTimeMs = 1

	var frameHandler aoiprxkit.FrameHandler
	src, ingress, err := BuildSource(cfg, Deps{
		AES67ReceiverFactory: func(_ aoiprxkit.Config, onFrame aoiprxkit.FrameHandler) (aoipad.ReceiverClient, error) {
			frameHandler = onFrame
			return &stubAES67Receiver{
				onStart: func() {
					frameHandler(aoiprxkit.PCMFrame{
						SequenceNumber: 1,
						SampleRateHz:   48000,
						Channels:       2,
						Samples:        []int32{7, -7, 9, -9},
						ReceivedAt:     time.Now(),
					})
				},
			}, nil
		},
	})
	if err != nil {
		t.Fatalf("build source: %v", err)
	}
	if src == nil {
		t.Fatalf("expected source for kind=aes67")
	}
	if ingress != nil {
		t.Fatalf("expected no ingress for kind=aes67")
	}

	sink := audio.NewStreamSource(128, cfg.Ingest.AES67.SampleRateHz)
	rt := ingest.NewRuntime(sink, src)
	if err := rt.Start(context.Background()); err != nil {
		t.Fatalf("runtime start: %v", err)
	}
	defer rt.Stop()

	waitForSinkFrames(t, sink, 2)

	stats := rt.Stats()
	if stats.Active.Kind != "aes67" {
		t.Fatalf("active kind=%q want aes67", stats.Active.Kind)
	}
	if stats.Source.ChunksIn != 1 {
		t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn)
	}
	if stats.Source.SamplesIn != 4 {
		t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn)
	}
}

func waitForSinkFrames(t *testing.T, sink *audio.StreamSource, minFrames int) {
	t.Helper()
	deadline := time.Now().Add(1 * time.Second)
	for time.Now().Before(deadline) {
		if sink.Available() >= minFrames {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatalf("timeout waiting for sink frames: have=%d want>=%d", sink.Available(), minFrames)
}
