use std::collections::HashMap;

use rottlib::arena::Arena;
use rottlib::diagnostics::{Diagnostic, Severity};
use rottlib::lexer::{TokenPosition, TokenSpan, TokenizedFile};
use rottlib::parser::Parser;

mod block_items;
mod control_flow_expressions;
mod declarations;
mod primary_expressions;
mod selector_expressions;
mod switch_expressions;

#[derive(Debug)]
pub(super) struct ExpectedLabel {
    pub span: TokenSpan,
    pub message: &'static str,
}

#[derive(Debug)]
pub(super) struct ExpectedDiagnostic<'a> {
    pub headline: &'static str,
    pub severity: Severity,
    pub code: Option<&'static str>,
    pub primary_label: Option<ExpectedLabel>,
    pub secondary_labels: &'a [ExpectedLabel],
    pub help: Option<&'static str>,
    pub notes: &'a [&'static str],
}

#[track_caller]
pub(super) fn assert_diagnostic(actual: &Diagnostic, expected: &ExpectedDiagnostic<'_>) {
    assert_eq!(actual.headline(), expected.headline);
    assert_eq!(actual.severity(), expected.severity);
    assert_eq!(actual.code(), expected.code);
    assert_eq!(actual.help(), expected.help);

    match (actual.primary_label(), expected.primary_label.as_ref()) {
        (None, None) => {}
        (Some(actual), Some(expected)) => {
            assert_eq!(actual.span, expected.span);
            assert_eq!(actual.message, expected.message);
        }
        _ => panic!("primary label mismatch"),
    }

    let actual_secondary = actual.secondary_labels();
    assert_eq!(actual_secondary.len(), expected.secondary_labels.len());

    for (actual, expected) in actual_secondary
        .iter()
        .zip(expected.secondary_labels.iter())
    {
        assert_eq!(actual.span, expected.span);
        assert_eq!(actual.message, expected.message);
    }

    let actual_notes = actual.notes();
    assert_eq!(actual_notes.len(), expected.notes.len());

    for (actual, expected) in actual_notes.iter().zip(expected.notes.iter()) {
        assert_eq!(actual, expected);
    }
}

#[derive(Debug, Clone, Copy)]
pub(super) struct Fixture {
    pub label: &'static str,
    pub source: &'static str,
}

pub(super) type FixtureRun = Vec<Diagnostic>;

pub(super) struct FixtureRuns {
    runs: HashMap<&'static str, FixtureRun>,
}

impl FixtureRuns {
    #[track_caller]
    pub fn get(&self, label: &str) -> Option<Vec<Diagnostic>> {
        self.runs.get(label).map(|fixture_run| fixture_run.clone())
    }

    #[track_caller]
    pub fn get_any(&self, label: &str) -> Diagnostic {
        self.runs
            .get(label)
            .map(|fixture_run| fixture_run[0].clone())
            .unwrap()
    }

    #[track_caller]
    pub fn get_by_code(&self, label: &str, code: &str) -> Diagnostic {
        self.runs
            .get(label)
            .unwrap_or_else(|| panic!("no fixture run for `{label}`"))
            .iter()
            .find(|diagnostic| diagnostic.code().as_deref() == Some(code))
            .unwrap_or_else(|| panic!("no `{code}` diagnostic in fixture `{label}`"))
            .clone()
    }
}

fn run_fixture(fixture: &'static Fixture) -> FixtureRun {
    let arena = Arena::new();
    let file = TokenizedFile::tokenize(fixture.source);
    let mut parser = Parser::new(&file, &arena);

    let _ = parser.parse_expression();
    let diagnostics = parser.diagnostics.clone();

    for diagnostic in &diagnostics {
        diagnostic.render(&file, fixture.label);
        println!();
    }

    diagnostics
}

pub(super) fn run_fixtures(fixtures: &'static [Fixture]) -> FixtureRuns {
    let mut runs = HashMap::new();

    for fixture in fixtures {
        runs.insert(fixture.label, run_fixture(fixture));
    }

    FixtureRuns { runs }
}
