package transcript

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"os"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

func TestParseValidMinimalTranscript(t *testing.T) {
	raw := readFixture(t, "testdata/valid_minimal.json")

	doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err != nil {
		t.Fatalf("Parse() error = %v, want nil", err)
	}

	if doc.ID != "session-alpha" {
		t.Fatalf("doc.ID = %q, want session-alpha", doc.ID)
	}
	if doc.Kind != DocumentKind {
		t.Fatalf("doc.Kind = %q, want %q", doc.Kind, DocumentKind)
	}
	if doc.Format != Format {
		t.Fatalf("doc.Format = %q, want %q", doc.Format, Format)
	}
	if doc.Digest == testDigest(raw) || !strings.HasPrefix(doc.Digest, "sha256:") {
		t.Fatalf("doc.Digest = %q, want canonical source digest distinct from raw input digest", doc.Digest)
	}
	if got := doc.Metadata["title"]; got != "Synthetic session transcript" {
		t.Fatalf("doc.Metadata[title] = %#v, want Synthetic session transcript", got)
	}
	if len(doc.Units) != 2 {
		t.Fatalf("len(doc.Units) = %d, want 2", len(doc.Units))
	}
	for i, unit := range doc.Units {
		want := source.SourceRef{SourceID: doc.ID, StartUnitID: unit.ID, EndUnitID: unit.ID}
		if unit.Ref != want {
			t.Fatalf("doc.Units[%d].Ref = %#v, want %#v", i, unit.Ref, want)
		}
	}

	first := doc.Units[0]
	if first.ID != 1 {
		t.Fatalf("first.ID = %d, want 1", first.ID)
	}
	if first.Kind != UnitKind {
		t.Fatalf("first.Kind = %q, want %q", first.Kind, UnitKind)
	}
	if first.Text != "The stone door opens." {
		t.Fatalf("first.Text = %q, want fixture text", first.Text)
	}
	if speaker, ok := Speaker(first); !ok || speaker != "Narrator" {
		t.Fatalf("Speaker(first) = %q, %v; want Narrator, true", speaker, ok)
	}
	if start, ok := Start(first); !ok || start != json.Number("0") {
		t.Fatalf("Start(first) = %q, %v; want 0, true", start, ok)
	}
	if end, ok := End(first); !ok || end != json.Number("4.5") {
		t.Fatalf("End(first) = %q, %v; want 4.5, true", end, ok)
	}

	ref := source.SourceRef{
		SourceID:    doc.ID,
		StartUnitID: doc.Units[0].ID,
		EndUnitID:   doc.Units[len(doc.Units)-1].ID,
	}
	if err := source.ValidateRef(doc, ref); err != nil {
		t.Fatalf("ValidateRef() error = %v, want nil", err)
	}
}

func TestParseAcceptsNumericSegmentIDs(t *testing.T) {
	raw := []byte(`{"metadata":{"application":"seriatim","version":"v1.5.0","output_schema":"seriatim-intermediate"},"segments":[{"id":1,"start":451.821,"end":469.685,"speaker":"Narrator","text":"The stone door opens.","categories":["scene"]},{"id":2,"start":470,"end":471,"speaker":"Player","text":"I step inside."}]}`)

	doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err != nil {
		t.Fatalf("Parse() error = %v, want nil", err)
	}
	if got, want := doc.Metadata["output_schema"], "seriatim-intermediate"; got != want {
		t.Fatalf("doc.Metadata[output_schema] = %#v, want %q", got, want)
	}
	if doc.Format != Format {
		t.Fatalf("doc.Format = %q, want %q", doc.Format, Format)
	}
	if len(doc.Units) != 2 {
		t.Fatalf("len(doc.Units) = %d, want 2", len(doc.Units))
	}
	if doc.Units[0].ID != 1 || doc.Units[1].ID != 2 {
		t.Fatalf("unit IDs = %#v, want numeric IDs", []int{doc.Units[0].ID, doc.Units[1].ID})
	}
	for i, unit := range doc.Units {
		want := source.SourceRef{SourceID: doc.ID, StartUnitID: unit.ID, EndUnitID: unit.ID}
		if unit.Ref != want {
			t.Fatalf("doc.Units[%d].Ref = %#v, want %#v", i, unit.Ref, want)
		}
	}
	ref := source.SourceRef{
		SourceID:    doc.ID,
		StartUnitID: 1,
		EndUnitID:   2,
	}
	if err := source.ValidateRef(doc, ref); err != nil {
		t.Fatalf("ValidateRef() error = %v, want nil", err)
	}
}

func TestParseRequestSourceIDOverridesMetadataIDs(t *testing.T) {
	raw := readFixture(t, "testdata/valid_minimal.json")

	doc, err := New().Parse(context.Background(), contracts.ParseRequest{
		SourceID: " requested-source ",
		Raw:      raw,
	})
	if err != nil {
		t.Fatalf("Parse() error = %v, want nil", err)
	}
	if doc.ID != "requested-source" {
		t.Fatalf("doc.ID = %q, want requested-source", doc.ID)
	}
}

func TestParseFallbackDocumentIDIsDeterministic(t *testing.T) {
	raw := []byte(`{"metadata":{},"segments":[{"id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]}`)

	first, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err != nil {
		t.Fatalf("first Parse() error = %v, want nil", err)
	}
	second, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err != nil {
		t.Fatalf("second Parse() error = %v, want nil", err)
	}

	if first.ID != second.ID {
		t.Fatalf("fallback IDs differ: %q vs %q", first.ID, second.ID)
	}
	if !strings.HasPrefix(first.ID, "seriatim:") {
		t.Fatalf("fallback ID = %q, want seriatim prefix", first.ID)
	}
	if first.ID != "seriatim:"+strings.TrimPrefix(testDigest(raw), "sha256:")[:16] {
		t.Fatalf("fallback ID = %q, want digest-derived ID", first.ID)
	}
}

func TestParseUsesMetadataSourceIDWhenMetadataIDIsAbsent(t *testing.T) {
	raw := []byte(`{"metadata":{"source_id":" source-from-metadata "},"segments":[{"id":1,"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."}]}`)

	doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err != nil {
		t.Fatalf("Parse() error = %v, want nil", err)
	}
	if doc.ID != "source-from-metadata" {
		t.Fatalf("doc.ID = %q, want source-from-metadata", doc.ID)
	}
}

func TestParseRejectsInvalidInput(t *testing.T) {
	tests := []struct {
		name    string
		raw     []byte
		wantErr []string
	}{
		{
			name:    "malformed JSON",
			raw:     []byte(`{"metadata":`),
			wantErr: []string{"seriatim input", "parse JSON"},
		},
		{
			name:    "trailing JSON",
			raw:     []byte(`{"metadata":{},"segments":[]} {}`),
			wantErr: []string{"seriatim input", "trailing"},
		},
		{
			name:    "missing metadata",
			raw:     []byte(`{"segments":[]}`),
			wantErr: []string{"metadata"},
		},
		{
			name:    "null metadata",
			raw:     []byte(`{"metadata":null,"segments":[]}`),
			wantErr: []string{"metadata", "object"},
		},
		{
			name:    "metadata wrong type",
			raw:     []byte(`{"metadata":[],"segments":[]}`),
			wantErr: []string{"metadata", "object"},
		},
		{
			name:    "missing segments",
			raw:     []byte(`{"metadata":{}}`),
			wantErr: []string{"segments"},
		},
		{
			name:    "null segments",
			raw:     []byte(`{"metadata":{},"segments":null}`),
			wantErr: []string{"segments", "array"},
		},
		{
			name:    "segments wrong type",
			raw:     []byte(`{"metadata":{},"segments":{}}`),
			wantErr: []string{"segments", "array"},
		},
		{
			name:    "empty segments",
			raw:     []byte(`{"metadata":{},"segments":[]}`),
			wantErr: []string{"segments", "empty"},
		},
		{
			name:    "missing segment id",
			raw:     validJSONWithSegment(`"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"id", "positive"},
		},
		{
			name:    "invalid segment id type",
			raw:     validJSONWithSegment(`"id":{},"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"id", "string or number"},
		},
		{
			name:    "whitespace segment id",
			raw:     validJSONWithSegment(`"id":" 1 ","start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"id", "whitespace"},
		},
		{
			name:    "empty text",
			raw:     validJSONWithSegment(`"id":1,"start":0,"end":1,"speaker":"Narrator","text":"   "`),
			wantErr: []string{"text", "empty"},
		},
		{
			name:    "missing speaker",
			raw:     validJSONWithSegment(`"id":1,"start":0,"end":1,"text":"Synthetic text."`),
			wantErr: []string{"speaker", "empty"},
		},
		{
			name:    "missing start",
			raw:     validJSONWithSegment(`"id":1,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"start", "empty"},
		},
		{
			name:    "missing end",
			raw:     validJSONWithSegment(`"id":1,"start":0,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"end", "empty"},
		},
		{
			name:    "negative start",
			raw:     validJSONWithSegment(`"id":1,"start":-1,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"start", "negative"},
		},
		{
			name:    "non-numeric end",
			raw:     validJSONWithSegment(`"id":1,"start":0,"end":"late","speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"segment[0]", "end", "number"},
		},
		{
			name:    "non-finite timestamp",
			raw:     validJSONWithSegment(`"id":1,"start":1e10000,"end":1e10000,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"start", "valid number"},
		},
		{
			name:    "end before start",
			raw:     validJSONWithSegment(`"id":1,"start":2,"end":1,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"end", "start"},
		},
		{
			name:    "end before start beyond float precision",
			raw:     validJSONWithSegment(`"id":1,"start":9007199254740993,"end":9007199254740992,"speaker":"Narrator","text":"Synthetic text."`),
			wantErr: []string{"end", "start"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: tt.raw})
			if err == nil {
				t.Fatal("Parse() error = nil, want error")
			}
			for _, want := range tt.wantErr {
				if !strings.Contains(err.Error(), want) {
					t.Fatalf("Parse() error = %q, want substring %q", err.Error(), want)
				}
			}
		})
	}
}

func TestParseRejectsDuplicateSegmentIDs(t *testing.T) {
	raw := readFixture(t, "testdata/duplicate_segment_id.json")

	_, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw})
	if err == nil {
		t.Fatal("Parse() error = nil, want duplicate ID error")
	}
	if !strings.Contains(err.Error(), "duplicated") || !strings.Contains(err.Error(), "1") {
		t.Fatalf("Parse() error = %q, want duplicate segment context", err.Error())
	}
}

func TestParseRejectsInvalidContextOrEmptyInput(t *testing.T) {
	if _, err := New().Parse(nil, contracts.ParseRequest{Raw: []byte(`{}`)}); err == nil {
		t.Fatal("Parse(nil context) error = nil, want error")
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := New().Parse(ctx, contracts.ParseRequest{Raw: []byte(`{}`)}); err == nil {
		t.Fatal("Parse(canceled context) error = nil, want error")
	}

	if _, err := New().Parse(context.Background(), contracts.ParseRequest{}); err == nil {
		t.Fatal("Parse(empty input) error = nil, want error")
	}
}

func readFixture(t *testing.T, path string) []byte {
	t.Helper()
	raw, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)
	}
	return raw
}

func validJSONWithSegment(segmentFields string) []byte {
	return []byte(`{"metadata":{"id":"fixture"},"segments":[{` + segmentFields + `}]}`)
}

func testDigest(raw []byte) string {
	sum := sha256.Sum256(raw)
	return "sha256:" + hex.EncodeToString(sum[:])
}
