package sat

import (
	"os"
	"path/filepath"
	"testing"
)

// The shipped list has to be readable and consistent — it is embedded, so a
// mistake in it is a mistake in every build.
func TestShippedBirds(t *testing.T) {
	b := &Birds{}
	if err := b.parse(shippedBirds); err != nil {
		t.Fatalf("birds.json does not parse: %v", err)
	}
	if b.Len() < 5 {
		t.Fatalf("only %d satellites shipped", b.Len())
	}
	for _, bird := range b.All() {
		if len(bird.Transponders) == 0 {
			t.Errorf("%s has no transponder", bird.Name)
		}
		for _, tr := range bird.Transponders {
			if tr.DownLo <= 0 {
				t.Errorf("%s / %s: no downlink", bird.Name, tr.Label)
			}
			if tr.DownHi != 0 && tr.DownHi <= tr.DownLo {
				t.Errorf("%s / %s: downlink passband runs backwards", bird.Name, tr.Label)
			}
			if tr.UpHi != 0 && tr.UpHi <= tr.UpLo {
				t.Errorf("%s / %s: uplink passband runs backwards", bird.Name, tr.Label)
			}
			// A linear transponder whose two passbands are different widths cannot
			// map one onto the other, and the split would drift across the pass.
			if tr.Linear() && (tr.DownHi-tr.DownLo) != (tr.UpHi-tr.UpLo) {
				t.Errorf("%s / %s: passbands are %d and %d Hz wide",
					bird.Name, tr.Label, tr.DownHi-tr.DownLo, tr.UpHi-tr.UpLo)
			}
		}
	}
}

func TestFindByAlias(t *testing.T) {
	b := &Birds{}
	if err := b.parse(shippedBirds); err != nil {
		t.Fatal(err)
	}
	// Every spelling on the left is one an operator or a feed actually uses.
	for _, tc := range []struct{ query, want string }{
		{"AO-91", "AO-91"},
		{"RADFXSAT (FOX-1B)", "AO-91"},
		{"radfxsat", "AO-91"},
		{"ISS (ZARYA)", "ISS (ZARYA)"},
		{"ISS", "ISS (ZARYA)"},
		{"SAUDISAT 1C (SO-50)", "SO-50"},
		{"so 50", "SO-50"},
		{"QO-100", "QO-100"},
		{"ESHAIL-2", "QO-100"},
		{"Es'hail 2", "QO-100"},
	} {
		got, ok := b.Find(tc.query)
		if !ok {
			t.Errorf("%q was not found", tc.query)
			continue
		}
		if got.Name != tc.want {
			t.Errorf("%q found %q, wanted %q", tc.query, got.Name, tc.want)
		}
	}
	if _, ok := b.Find("NOAA 15"); ok {
		t.Error("a weather satellite should not carry an amateur frequency plan")
	}
}

// Matches is the other direction: a bird in hand, scanning a feed's names.
func TestBirdMatches(t *testing.T) {
	b := Bird{Name: "AO-91", Aliases: []string{"RADFXSAT", "FOX-1B"}}
	for _, feed := range []string{"AO-91", "RADFXSAT (FOX-1B)", "radfxsat", "FOX 1B"} {
		if !b.Matches(feed) {
			t.Errorf("%q was not recognised as AO-91", feed)
		}
	}
	for _, feed := range []string{"AO-92", "NOAA 15", "FOX-1A"} {
		if b.Matches(feed) {
			t.Errorf("%q was wrongly taken for AO-91", feed)
		}
	}
	// A bracketed catalogue name matched from the other side.
	iss := Bird{Name: "ISS (ZARYA)"}
	if !iss.Matches("ISS") || !iss.Matches("ZARYA") {
		t.Error("the ISS was not recognised by either half of its catalogue name")
	}
}

// The uplink maths is the part that matters on the air: a station worked at one
// end of an inverting transponder has to be answered at the other.
func TestUplinkFor(t *testing.T) {
	inv := Transponder{
		DownLo: 435800000, DownHi: 435900000,
		UpLo: 145900000, UpHi: 146000000,
		Inverting: true,
	}
	straight := Transponder{
		DownLo: 29400000, DownHi: 29500000,
		UpLo: 145850000, UpHi: 145950000,
	}
	fm := Transponder{DownLo: 436795000, UpLo: 145850000}

	for _, tc := range []struct {
		name string
		tr   Transponder
		down int64
		want int64
	}{
		{"inverting, bottom of the downlink", inv, 435800000, 146000000},
		{"inverting, top of the downlink", inv, 435900000, 145900000},
		{"inverting, 30 kHz up", inv, 435830000, 145970000},
		{"straight, bottom", straight, 29400000, 145850000},
		{"straight, 25 kHz up", straight, 29425000, 145875000},
		{"FM channel ignores the tuned downlink", fm, 436798000, 145850000},
		{"below the passband is clamped", inv, 435700000, 146000000},
		{"above the passband is clamped", inv, 436000000, 145900000},
	} {
		if got := tc.tr.UplinkFor(tc.down); got != tc.want {
			t.Errorf("%s: got %d, wanted %d", tc.name, got, tc.want)
		}
	}

	// Receive-only: a beacon has nothing to answer on.
	if got := (Transponder{DownLo: 145800000}).UplinkFor(145800000); got != 0 {
		t.Errorf("a receive-only transponder gave an uplink of %d", got)
	}
}

// Whichever end the operator takes hold of, the pair has to agree.
func TestDownlinkForRoundTrip(t *testing.T) {
	for _, tr := range []Transponder{
		{DownLo: 435800000, DownHi: 435900000, UpLo: 145900000, UpHi: 146000000, Inverting: true},
		{DownLo: 29400000, DownHi: 29500000, UpLo: 145850000, UpHi: 145950000},
	} {
		for _, down := range []int64{tr.DownLo, tr.Centre(), tr.DownHi} {
			if got := tr.DownlinkFor(tr.UplinkFor(down)); got != down {
				t.Errorf("inverting=%v: %d → uplink → %d", tr.Inverting, down, got)
			}
		}
	}
}

func TestLoadBirdsWritesTheEditableCopy(t *testing.T) {
	dir := t.TempDir()
	b, err := LoadBirds(dir)
	if err != nil {
		t.Fatal(err)
	}
	shipped := b.Len()
	path := filepath.Join(dir, BirdsName)
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("the editable copy was not written: %v", err)
	}

	// An operator's own list is what gets used from then on.
	mine := `[{"name":"MY-SAT","transponders":[{"label":"FM","mode":"FM","down_lo":1,"up_lo":2}]}]`
	if err := os.WriteFile(path, []byte(mine), 0o644); err != nil {
		t.Fatal(err)
	}
	b, err = LoadBirds(dir)
	if err != nil {
		t.Fatal(err)
	}
	if b.Len() != 1 {
		t.Fatalf("the operator's list was not used: %d satellites", b.Len())
	}

	// And a broken one falls back without destroying what they wrote.
	if err := os.WriteFile(path, []byte("[{oops"), 0o644); err != nil {
		t.Fatal(err)
	}
	b, err = LoadBirds(dir)
	if err == nil {
		t.Error("a broken list was accepted silently")
	}
	if b.Len() != shipped {
		t.Errorf("the shipped list did not take over: %d satellites", b.Len())
	}
	if data, _ := os.ReadFile(path); string(data) != "[{oops" {
		t.Error("the operator's broken file was overwritten")
	}
}
