package dxcc

import (
	"strings"
	"testing"
)

const sampleCty = `Sov Mil Order of Malta:   15:  28:  EU:   41.90:   -12.43:    -1.0:  1A:
    1A;
Monaco:                   14:  27:  EU:   43.73:    -7.40:    -1.0:  3A:
    3A;
France:                   14:  27:  EU:   46.00:    -2.00:    -1.0:  F:
    F,HW,HX,HY,TH,TM,TO,TP,TQ,TV,TX;
Germany:                  14:  28:  EU:   51.00:   -10.00:    -1.0:  DL:
    DA,DB,DC,DD,DE,DF,DG,DH,DI,DJ,DK,DL,DM,DN,DO,DP,DQ,DR;
United States:            05:  08:  NA:   37.53:    91.67:     5.0:  K:
    =W1AW(5)[7],K,N,W,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK;
`

// cty.dat stores longitude "+ for West"; we normalise to the standard
// "+ for East" so bearing/distance math (and the cluster map) aren't mirrored.
// Regression guard for the Svalbard-azimuth bug (356° instead of 4°).
func TestLongitudeSign(t *testing.T) {
	db, err := Load(strings.NewReader(sampleCty))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	if m, ok := db.Lookup("F4BPO"); !ok || m.Lon <= 0 { // France 2°E → +2
		t.Errorf("France lon = %v, want positive (East)", m.Lon)
	}
	if m, ok := db.Lookup("K1ABC"); !ok || m.Lon >= 0 { // USA 92°W → ~-91.67
		t.Errorf("USA lon = %v, want negative (West)", m.Lon)
	}
}

func TestLookup(t *testing.T) {
	db, err := Load(strings.NewReader(sampleCty))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	cases := []struct {
		call    string
		wantEnt string
	}{
		{"F4NIE", "France"},
		{"F4BPO", "France"},
		{"F4BPO/P", "France"},
		{"DL/F4NIE", "Germany"},
		{"DL5XYZ", "Germany"},
		{"K1ABC", "United States"},
		{"N0CALL", "United States"},
		{"3A2MD", "Monaco"},
		{"W1AW", "United States"}, // exact match wins
	}
	for _, c := range cases {
		m, ok := db.Lookup(c.call)
		if !ok {
			t.Errorf("%s: no match", c.call)
			continue
		}
		if m.Entity.Name != c.wantEnt {
			t.Errorf("%s: got %q, want %q", c.call, m.Entity.Name, c.wantEnt)
		}
	}

	// W1AW exact match has CQ override 5 and ITU override 7.
	m, _ := db.Lookup("W1AW")
	if m.CQZone != 5 || m.ITUZone != 7 {
		t.Errorf("W1AW overrides: got CQ=%d ITU=%d, want 5/7", m.CQZone, m.ITUZone)
	}
}

// A "/N" call-area suffix can change the DXCC entity: HD5MW/8 re-homes to the
// HD8 area (Galápagos), not the base call's Ecuador.
func TestCallAreaSuffix(t *testing.T) {
	const cty = `Ecuador:                  10:  12:  SA:   -1.40:    78.40:     5.0:  HC:
    HC,HD;
Galapagos Islands:        10:  12:  SA:    0.00:    91.00:     6.0:  HC8:
    HC8,HD8;
`
	db, err := Load(strings.NewReader(cty))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	cases := map[string]string{
		"HD5MW":   "Ecuador",
		"HD5MW/8": "Galapagos Islands",
		"HC2AO":   "Ecuador",
		"HD8M":    "Galapagos Islands",
		"HC1WW/8": "Galapagos Islands",
	}
	for call, want := range cases {
		m, ok := db.Lookup(call)
		if !ok {
			t.Errorf("%s: no match", call)
			continue
		}
		if m.Entity.Name != want {
			t.Errorf("%s: got %q, want %q", call, m.Entity.Name, want)
		}
	}
}

// US/VK call-district zone refinement (W6 = CQ3/ITU6, not the entity default).
func TestZoneByCallDistrict(t *testing.T) {
	cases := []struct {
		adif      int
		call      string
		cqz, ituz int
		ok        bool
	}{
		{291, "W6XYZ", 3, 6, true},
		{291, "K7AB", 3, 6, true},
		{291, "W4ABC", 5, 8, true},
		{291, "N0CALL", 4, 7, true},
		{291, "AA5XX", 4, 7, true},
		{150, "VK6AA", 29, 58, true}, // West Australia
		{150, "VK3XY", 30, 59, true}, // Victoria
		{230, "DL1ABC", 0, 0, false}, // Germany: no district rule
		{291, "WABC", 0, 0, false},   // no digit
	}
	for _, c := range cases {
		cqz, ituz, ok := ZoneByCallDistrict(c.adif, c.call)
		if ok != c.ok || (ok && (cqz != c.cqz || ituz != c.ituz)) {
			t.Errorf("ZoneByCallDistrict(%d,%q) = %d/%d ok=%v, want %d/%d ok=%v",
				c.adif, c.call, cqz, ituz, ok, c.cqz, c.ituz, c.ok)
		}
	}
}

// KG4 is Guantanamo Bay only with a 2-character suffix (KG4XX); 1- or 3-char
// suffixes (KG4W, KG4ABC) are continental USA. cty.dat carries a bare "KG4"
// prefix, so the resolver must apply the suffix-length rule.
func TestKG4SuffixRule(t *testing.T) {
	const cty = `United States:            05:  08:  NA:   37.53:    91.67:     5.0:  K:
    K,N,W;
Guantanamo Bay:           08:  11:  NA:   19.92:    75.18:    -5.0:  KG4:
    KG4;
`
	db, err := Load(strings.NewReader(cty))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	cases := map[string]string{
		"KG4W":   "United States",  // 1-char suffix
		"KG4AA":  "Guantanamo Bay", // 2-char suffix
		"KG4ABC": "United States",  // 3-char suffix
		"KG4":    "United States",  // no suffix
		"KG4W/P": "United States",  // modifier stripped, still 1-char
	}
	for call, want := range cases {
		m, ok := db.Lookup(call)
		if !ok {
			t.Errorf("%s: no match", call)
			continue
		}
		if m.Entity.Name != want {
			t.Errorf("%s: got %q, want %q", call, m.Entity.Name, want)
		}
	}
}

// cty.dat marks non-DXCC entities (Sicily *IT9, African Italy *IG9) with a
// leading '*'; the parser must fold those into their parent DXCC entity
// "Italy" while leaving real DXCC entities — Sardinia (IS0), no '*' — alone.
func TestCanonicalEntityNames(t *testing.T) {
	const cty = `Italy:                    15:  28:  EU:   42.82:   -12.58:    -1.0:  I:
    I,IK,IZ;
African Italy:            33:  37:  AF:   35.67:   -12.67:    -1.0:  *IG9:
    IG9,IH9;
Sardinia:                 15:  28:  EU:   40.15:    -9.27:    -1.0:  IS0:
    IM0,IS,IW0U,IW0V;
Sicily:                   15:  28:  EU:   37.50:   -14.00:    -1.0:  *IT9:
    IT9,IW9;
`
	db, err := Load(strings.NewReader(cty))
	if err != nil {
		t.Fatalf("load: %v", err)
	}
	cases := map[string]string{
		"IW9EZO": "Italy", // Sicily (*IT9) → Italy
		"IT9CLY": "Italy",
		"IG9A":   "Italy", // African Italy (*IG9) → Italy
		"IK0ABC": "Italy",
		"IS0XYZ": "Sardinia", // real DXCC entity — must stay Sardinia
		"IM0ABC": "Sardinia",
	}
	for call, want := range cases {
		m, ok := db.Lookup(call)
		if !ok {
			t.Errorf("%s: no match", call)
			continue
		}
		if m.Entity.Name != want {
			t.Errorf("%s: country = %q, want %q", call, m.Entity.Name, want)
		}
	}
	// African Italy keeps its own zones/continent even though it reports
	// Italy as the entity.
	if m, _ := db.Lookup("IG9A"); m.CQZone != 33 || m.Continent != "AF" {
		t.Errorf("IG9A: got CQ=%d cont=%s, want 33/AF", m.CQZone, m.Continent)
	}
	if EntityDXCC("Sicily") != 248 {
		t.Errorf("EntityDXCC(Sicily) = %d, want 248 (Italy)", EntityDXCC("Sicily"))
	}
	if EntityDXCC("Sardinia") != 225 {
		t.Errorf("EntityDXCC(Sardinia) = %d, want 225", EntityDXCC("Sardinia"))
	}
}

func TestNormalize(t *testing.T) {
	cases := map[string]string{
		"F4BPO":     "F4BPO",
		"f4bpo":     "F4BPO",
		" F4BPO ":   "F4BPO",
		"F4BPO/P":   "F4BPO",
		"F4BPO/MM":  "F4BPO",  // maritime mobile → strip, keep home entity for the log
		"F4BPO/AM":  "F4BPO",  // aeronautical mobile → strip, keep home entity for the log
		"F4BPO/M":   "F4BPO",  // plain mobile keeps the home entity
		"F4BPO/5":   "F5BPO",  // "/5" re-homes to call area 5
		"HD5MW/8":   "HD8MW",  // "/8" → Galápagos call area (HD8)
		"7X2ARA/4":  "7X4ARA", // "/4" changes the AREA digit (2→4), NOT the 7 of the 7X prefix (was wrongly 4X2ARA = Israel)
		"3A2ABC/6":  "3A6ABC", // digit-first prefix 3A: area digit is the 2, not the 3
		"4X1AB/2":   "4X2AB",  // 4X (Israel) area 1→2, keep the leading 4
		"DL/F4BPO":  "DL",
		"MM/KA9P":   "MM", // leading MM = Scotland operating prefix
		"MM/LY3X/P": "MM",
		"F4BPO/W6":  "W6",
		"VK9/F4BPO": "VK9",
		"4U1UN/B":   "4U1UN", // trailing /B = beacon → strip, keep the base call
		"B/F4BPO":   "B",     // leading B = China operating prefix, NOT a beacon
	}
	for in, want := range cases {
		if got := normalizeCallsign(in); got != want {
			t.Errorf("normalize(%q) = %q, want %q", in, got, want)
		}
	}
}
