//! DAO governance with on-chain voting for Synor.
//!
//! Implements:
//! - Proposal creation and voting
//! - Token-weighted voting (1 SYNOR = 1 vote)
//! - Optional quadratic voting
//! - Quorum requirements
//! - Time-locked execution
//!
//! # Governance Flow
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │                    PROPOSAL LIFECYCLE                        │
//! └─────────────────────────────────────────────────────────────┘
//!
//!   1. PROPOSAL         2. VOTING           3. EXECUTION
//!   ─────────────       ─────────────       ─────────────
//!
//!   User creates     →  Token holders   →  If passed:
//!   proposal with       vote YES/NO/        execute after
//!   required tokens     ABSTAIN             time delay
//!
//!   ┌─────────┐        ┌─────────┐        ┌─────────┐
//!   │ Pending │   →    │ Active  │   →    │ Passed  │   →   Executed
//!   └─────────┘        └─────────┘        └─────────┘
//!        │                  │                  │
//!        └──────────────────┴──────────────────┘
//!                           │
//!                           ▼
//!                      [Defeated/Expired]
//! ```

use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use synor_types::{Address, Hash256};
use thiserror::Error;

/// Unique identifier for a proposal.
pub type ProposalId = Hash256;

/// Voting configuration.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct VotingConfig {
    /// Minimum tokens required to create a proposal.
    pub proposal_threshold: u64,
    /// Minimum participation for quorum (basis points, 1000 = 10%).
    pub quorum_bps: u32,
    /// Voting period in blocks.
    pub voting_period_blocks: u64,
    /// Delay before voting starts (blocks).
    pub voting_delay_blocks: u64,
    /// Delay after passing before execution (blocks).
    pub execution_delay_blocks: u64,
    /// Whether to use quadratic voting.
    pub quadratic_voting: bool,
    /// Maximum votes per address (0 = unlimited).
    pub max_votes_per_address: u64,
}

impl Default for VotingConfig {
    fn default() -> Self {
        VotingConfig {
            proposal_threshold: 100_000 * 100_000_000, // 100,000 SYNOR
            quorum_bps: 1000,                          // 10%
            voting_period_blocks: 864_000,             // ~1 day at 10 bps
            voting_delay_blocks: 86_400,               // ~2.4 hours
            execution_delay_blocks: 172_800,           // ~4.8 hours
            quadratic_voting: false,
            max_votes_per_address: 0, // unlimited
        }
    }
}

impl VotingConfig {
    /// Configuration for faster governance (testing).
    pub fn fast() -> Self {
        VotingConfig {
            proposal_threshold: 1000 * 100_000_000,
            quorum_bps: 500,
            voting_period_blocks: 100,
            voting_delay_blocks: 10,
            execution_delay_blocks: 10,
            quadratic_voting: false,
            max_votes_per_address: 0,
        }
    }

    /// Configuration with quadratic voting.
    pub fn quadratic() -> Self {
        VotingConfig {
            quadratic_voting: true,
            ..Default::default()
        }
    }
}

/// Type of proposal.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub enum ProposalType {
    /// Transfer tokens from treasury.
    TreasurySpend {
        recipient: Address,
        amount: u64,
        reason: String,
    },
    /// Grant tokens to a project/team.
    EcosystemGrant {
        recipient: Address,
        amount: u64,
        project_name: String,
        milestones: Vec<String>,
    },
    /// Change a protocol parameter.
    ParameterChange {
        parameter: String,
        old_value: String,
        new_value: String,
    },
    /// Add/remove council member.
    CouncilChange {
        action: CouncilAction,
        member: Address,
        role: String,
    },
    /// Upgrade contract/protocol.
    Upgrade {
        contract_id: Hash256,
        new_code_hash: Hash256,
        description: String,
    },
    /// Create a new vesting contract.
    CreateVesting {
        beneficiary: Address,
        amount: u64,
        cliff_months: u32,
        vesting_months: u32,
        description: String,
    },
    /// Text-only proposal (signaling).
    Signaling {
        title: String,
        description: String,
        options: Vec<String>,
    },
    /// Custom proposal with arbitrary data.
    Custom { action_type: String, data: Vec<u8> },
}

/// Council member action.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub enum CouncilAction {
    Add,
    Remove,
    ChangeRole,
}

/// State of a proposal.
#[derive(
    Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize,
)]
pub enum ProposalState {
    /// Proposal is pending (voting hasn't started).
    Pending,
    /// Voting is active.
    Active,
    /// Proposal passed, awaiting execution.
    Passed,
    /// Proposal was defeated.
    Defeated,
    /// Proposal was executed.
    Executed,
    /// Proposal was cancelled by proposer.
    Cancelled,
    /// Proposal expired without execution.
    Expired,
}

impl ProposalState {
    /// Checks if the proposal is in a final state.
    pub fn is_final(&self) -> bool {
        matches!(
            self,
            ProposalState::Executed
                | ProposalState::Cancelled
                | ProposalState::Expired
                | ProposalState::Defeated
        )
    }
}

/// Vote choice.
#[derive(
    Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize,
)]
pub enum VoteChoice {
    /// Vote in favor.
    Yes,
    /// Vote against.
    No,
    /// Abstain (counts for quorum but not outcome).
    Abstain,
}

/// Voting power calculation.
#[derive(Clone, Copy, Debug)]
pub enum VotingPower {
    /// Linear: 1 token = 1 vote.
    Linear(u64),
    /// Quadratic: sqrt(tokens) votes.
    Quadratic(u64),
}

impl VotingPower {
    /// Calculates the actual vote weight.
    pub fn weight(&self) -> u64 {
        match self {
            VotingPower::Linear(tokens) => *tokens,
            VotingPower::Quadratic(tokens) => {
                // sqrt(tokens) - use integer sqrt
                (*tokens as f64).sqrt() as u64
            }
        }
    }
}

/// A vote cast on a proposal.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Vote {
    /// Voter address.
    pub voter: Address,
    /// Vote choice.
    pub choice: VoteChoice,
    /// Voting power (token balance at snapshot).
    pub power: u64,
    /// Actual vote weight (may differ from power in quadratic voting).
    pub weight: u64,
    /// Block when vote was cast.
    pub voted_at_block: u64,
    /// Optional reason/comment.
    pub reason: Option<String>,
}

/// A governance proposal.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Proposal {
    /// Unique proposal ID.
    pub id: ProposalId,
    /// Proposal number (sequential).
    pub number: u64,
    /// Proposer address.
    pub proposer: Address,
    /// Proposal type and details.
    pub proposal_type: ProposalType,
    /// Title.
    pub title: String,
    /// Description (markdown).
    pub description: String,
    /// Discussion link (forum, Discord, etc.).
    pub discussion_url: Option<String>,
    /// Block when proposal was created.
    pub created_at_block: u64,
    /// Block when voting starts.
    pub voting_starts_block: u64,
    /// Block when voting ends.
    pub voting_ends_block: u64,
    /// Block when execution is allowed (if passed).
    pub execution_allowed_block: u64,
    /// Current state.
    pub state: ProposalState,
    /// Votes cast.
    pub votes: HashMap<Address, Vote>,
    /// Total yes votes (weighted).
    pub yes_votes: u64,
    /// Total no votes (weighted).
    pub no_votes: u64,
    /// Total abstain votes (weighted).
    pub abstain_votes: u64,
    /// Total tokens at snapshot (for quorum calculation).
    pub total_supply_snapshot: u64,
    /// When executed (block number).
    pub executed_at: Option<u64>,
    /// Execution transaction ID.
    pub execution_tx: Option<Hash256>,
}

impl Proposal {
    /// Creates a new proposal.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        number: u64,
        proposer: Address,
        proposal_type: ProposalType,
        title: String,
        description: String,
        created_at_block: u64,
        config: &VotingConfig,
        total_supply: u64,
    ) -> Self {
        // Generate ID
        let mut id_data = Vec::new();
        id_data.extend_from_slice(&number.to_le_bytes());
        id_data.extend_from_slice(proposer.payload());
        id_data.extend_from_slice(&created_at_block.to_le_bytes());
        let id = Hash256::blake3(&id_data);

        let voting_starts_block = created_at_block + config.voting_delay_blocks;
        let voting_ends_block = voting_starts_block + config.voting_period_blocks;
        let execution_allowed_block = voting_ends_block + config.execution_delay_blocks;

        Proposal {
            id,
            number,
            proposer,
            proposal_type,
            title,
            description,
            discussion_url: None,
            created_at_block,
            voting_starts_block,
            voting_ends_block,
            execution_allowed_block,
            state: ProposalState::Pending,
            votes: HashMap::new(),
            yes_votes: 0,
            no_votes: 0,
            abstain_votes: 0,
            total_supply_snapshot: total_supply,
            executed_at: None,
            execution_tx: None,
        }
    }

    /// Updates proposal state based on current block.
    pub fn update_state(&mut self, current_block: u64, quorum_bps: u32) {
        // Don't update if in final state
        if self.state.is_final() {
            return;
        }

        // Check if voting should start
        if self.state == ProposalState::Pending && current_block >= self.voting_starts_block {
            self.state = ProposalState::Active;
        }

        // Check if voting ended
        if self.state == ProposalState::Active && current_block >= self.voting_ends_block {
            // Calculate if passed
            if self.has_quorum(quorum_bps) && self.yes_votes > self.no_votes {
                self.state = ProposalState::Passed;
            } else {
                self.state = ProposalState::Defeated;
            }
        }

        // Check if passed proposal expired
        if self.state == ProposalState::Passed {
            // Give 1 week to execute after allowed
            let expiry_block = self.execution_allowed_block + 864_000 * 7; // ~1 week
            if current_block > expiry_block {
                self.state = ProposalState::Expired;
            }
        }
    }

    /// Checks if quorum is reached.
    pub fn has_quorum(&self, quorum_bps: u32) -> bool {
        let total_votes = self.yes_votes + self.no_votes + self.abstain_votes;
        let required = (self.total_supply_snapshot as u128 * quorum_bps as u128 / 10000) as u64;
        total_votes >= required
    }

    /// Checks if an address has voted.
    pub fn has_voted(&self, voter: &Address) -> bool {
        self.votes.contains_key(voter)
    }

    /// Returns the vote for an address.
    pub fn get_vote(&self, voter: &Address) -> Option<&Vote> {
        self.votes.get(voter)
    }

    /// Returns voting participation rate.
    pub fn participation_rate(&self) -> f64 {
        let total_votes = self.yes_votes + self.no_votes + self.abstain_votes;
        if self.total_supply_snapshot == 0 {
            0.0
        } else {
            (total_votes as f64 / self.total_supply_snapshot as f64) * 100.0
        }
    }

    /// Returns the current result (yes percentage).
    pub fn yes_percentage(&self) -> f64 {
        let total = self.yes_votes + self.no_votes;
        if total == 0 {
            0.0
        } else {
            (self.yes_votes as f64 / total as f64) * 100.0
        }
    }

    /// Returns time remaining in blocks.
    pub fn time_remaining(&self, current_block: u64) -> Option<u64> {
        match self.state {
            ProposalState::Pending => Some(self.voting_starts_block.saturating_sub(current_block)),
            ProposalState::Active => Some(self.voting_ends_block.saturating_sub(current_block)),
            ProposalState::Passed => {
                Some(self.execution_allowed_block.saturating_sub(current_block))
            }
            _ => None,
        }
    }

    /// Returns proposal summary.
    pub fn summary(&self, current_block: u64, quorum_bps: u32) -> ProposalSummary {
        ProposalSummary {
            id: self.id,
            number: self.number,
            title: self.title.clone(),
            proposer: self.proposer.clone(),
            state: self.state,
            yes_votes: self.yes_votes,
            no_votes: self.no_votes,
            abstain_votes: self.abstain_votes,
            total_voters: self.votes.len(),
            yes_percentage: self.yes_percentage(),
            participation_rate: self.participation_rate(),
            has_quorum: self.has_quorum(quorum_bps),
            time_remaining_blocks: self.time_remaining(current_block),
        }
    }
}

/// Summary of a proposal.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProposalSummary {
    pub id: ProposalId,
    pub number: u64,
    pub title: String,
    pub proposer: Address,
    pub state: ProposalState,
    pub yes_votes: u64,
    pub no_votes: u64,
    pub abstain_votes: u64,
    pub total_voters: usize,
    pub yes_percentage: f64,
    pub participation_rate: f64,
    pub has_quorum: bool,
    pub time_remaining_blocks: Option<u64>,
}

/// DAO governance errors.
#[derive(Debug, Error)]
pub enum DaoError {
    #[error("Proposal not found")]
    ProposalNotFound,

    #[error("Insufficient voting power to create proposal")]
    InsufficientVotingPower,

    #[error("Voting not yet started")]
    VotingNotStarted,

    #[error("Voting has ended")]
    VotingEnded,

    #[error("Already voted")]
    AlreadyVoted,

    #[error("Proposal not passed")]
    NotPassed,

    #[error("Execution not yet allowed (timelock)")]
    ExecutionTooEarly,

    #[error("Proposal already executed")]
    AlreadyExecuted,

    #[error("Proposal cancelled")]
    Cancelled,

    #[error("Proposal expired")]
    Expired,

    #[error("Not authorized")]
    NotAuthorized,

    #[error("Invalid proposal")]
    InvalidProposal,
}

/// The DAO governance contract.
pub struct DAO {
    /// Configuration.
    pub config: VotingConfig,
    /// All proposals.
    proposals: HashMap<ProposalId, Proposal>,
    /// Proposal count (for numbering).
    proposal_count: u64,
    /// Proposals by proposer.
    by_proposer: HashMap<Address, Vec<ProposalId>>,
    /// Council members.
    council: Vec<Address>,
    /// Guardian (can cancel malicious proposals).
    guardian: Option<Address>,
}

impl DAO {
    /// Creates a new DAO.
    pub fn new(config: VotingConfig) -> Self {
        DAO {
            config,
            proposals: HashMap::new(),
            proposal_count: 0,
            by_proposer: HashMap::new(),
            council: Vec::new(),
            guardian: None,
        }
    }

    /// Sets the guardian address.
    pub fn set_guardian(&mut self, guardian: Address) {
        self.guardian = Some(guardian);
    }

    /// Adds a council member.
    pub fn add_council_member(&mut self, member: Address) {
        if !self.council.contains(&member) {
            self.council.push(member);
        }
    }

    /// Removes a council member.
    pub fn remove_council_member(&mut self, member: &Address) {
        self.council.retain(|m| m != member);
    }

    /// Creates a new proposal.
    #[allow(clippy::too_many_arguments)]
    pub fn create_proposal(
        &mut self,
        proposer: Address,
        proposer_balance: u64,
        proposal_type: ProposalType,
        title: String,
        description: String,
        current_block: u64,
        total_supply: u64,
    ) -> Result<ProposalId, DaoError> {
        // Check proposer has enough tokens
        if proposer_balance < self.config.proposal_threshold {
            return Err(DaoError::InsufficientVotingPower);
        }

        self.proposal_count += 1;

        let proposal = Proposal::new(
            self.proposal_count,
            proposer.clone(),
            proposal_type,
            title,
            description,
            current_block,
            &self.config,
            total_supply,
        );

        let id = proposal.id;

        // Index by proposer
        self.by_proposer.entry(proposer).or_default().push(id);

        self.proposals.insert(id, proposal);
        Ok(id)
    }

    /// Casts a vote on a proposal.
    pub fn vote(
        &mut self,
        proposal_id: &ProposalId,
        voter: Address,
        voter_balance: u64,
        choice: VoteChoice,
        current_block: u64,
        reason: Option<String>,
    ) -> Result<(), DaoError> {
        let proposal = self
            .proposals
            .get_mut(proposal_id)
            .ok_or(DaoError::ProposalNotFound)?;

        // Update state first
        proposal.update_state(current_block, self.config.quorum_bps);

        // Check voting is active
        if proposal.state == ProposalState::Pending {
            return Err(DaoError::VotingNotStarted);
        }
        if proposal.state != ProposalState::Active {
            return Err(DaoError::VotingEnded);
        }

        // Check not already voted
        if proposal.has_voted(&voter) {
            return Err(DaoError::AlreadyVoted);
        }

        // Calculate voting weight
        let power = voter_balance;
        let weight = if self.config.quadratic_voting {
            VotingPower::Quadratic(power).weight()
        } else {
            VotingPower::Linear(power).weight()
        };

        // Apply max votes limit if set
        let weight = if self.config.max_votes_per_address > 0 {
            weight.min(self.config.max_votes_per_address)
        } else {
            weight
        };

        // Create vote
        let vote = Vote {
            voter: voter.clone(),
            choice,
            power,
            weight,
            voted_at_block: current_block,
            reason,
        };

        // Update vote totals
        match choice {
            VoteChoice::Yes => proposal.yes_votes += weight,
            VoteChoice::No => proposal.no_votes += weight,
            VoteChoice::Abstain => proposal.abstain_votes += weight,
        }

        proposal.votes.insert(voter, vote);
        Ok(())
    }

    /// Executes a passed proposal.
    pub fn execute(
        &mut self,
        proposal_id: &ProposalId,
        _executor: &Address,
        current_block: u64,
    ) -> Result<&Proposal, DaoError> {
        let proposal = self
            .proposals
            .get_mut(proposal_id)
            .ok_or(DaoError::ProposalNotFound)?;

        // Update state
        proposal.update_state(current_block, self.config.quorum_bps);

        // Check state
        if proposal.state == ProposalState::Executed {
            return Err(DaoError::AlreadyExecuted);
        }
        if proposal.state == ProposalState::Cancelled {
            return Err(DaoError::Cancelled);
        }
        if proposal.state == ProposalState::Expired {
            return Err(DaoError::Expired);
        }
        if proposal.state != ProposalState::Passed {
            return Err(DaoError::NotPassed);
        }

        // Check timelock
        if current_block < proposal.execution_allowed_block {
            return Err(DaoError::ExecutionTooEarly);
        }

        // Mark as executed
        proposal.state = ProposalState::Executed;
        proposal.executed_at = Some(current_block);

        Ok(self.proposals.get(proposal_id).unwrap())
    }

    /// Cancels a proposal (only by proposer or guardian).
    pub fn cancel(
        &mut self,
        proposal_id: &ProposalId,
        canceller: &Address,
    ) -> Result<(), DaoError> {
        let proposal = self
            .proposals
            .get_mut(proposal_id)
            .ok_or(DaoError::ProposalNotFound)?;

        // Check authorization
        let is_proposer = &proposal.proposer == canceller;
        let is_guardian = self.guardian.as_ref() == Some(canceller);

        if !is_proposer && !is_guardian {
            return Err(DaoError::NotAuthorized);
        }

        // Cannot cancel executed proposals
        if proposal.state == ProposalState::Executed {
            return Err(DaoError::AlreadyExecuted);
        }

        proposal.state = ProposalState::Cancelled;
        Ok(())
    }

    /// Gets a proposal by ID.
    pub fn get_proposal(&self, id: &ProposalId) -> Option<&Proposal> {
        self.proposals.get(id)
    }

    /// Gets all proposals by a proposer.
    pub fn get_by_proposer(&self, proposer: &Address) -> Vec<&Proposal> {
        self.by_proposer
            .get(proposer)
            .map(|ids| ids.iter().filter_map(|id| self.proposals.get(id)).collect())
            .unwrap_or_default()
    }

    /// Gets all active proposals.
    pub fn active_proposals(&self, current_block: u64) -> Vec<&Proposal> {
        self.proposals
            .values()
            .filter(|p| {
                p.state == ProposalState::Active
                    || (p.state == ProposalState::Pending && current_block >= p.voting_starts_block)
            })
            .collect()
    }

    /// Gets all proposals in a given state.
    pub fn proposals_by_state(&self, state: ProposalState) -> Vec<&Proposal> {
        self.proposals
            .values()
            .filter(|p| p.state == state)
            .collect()
    }

    /// Updates all proposal states.
    pub fn update_all_states(&mut self, current_block: u64) {
        for proposal in self.proposals.values_mut() {
            proposal.update_state(current_block, self.config.quorum_bps);
        }
    }

    /// Returns governance statistics.
    pub fn stats(&self) -> DaoStats {
        let proposals: Vec<_> = self.proposals.values().collect();

        DaoStats {
            total_proposals: proposals.len() as u64,
            active_proposals: proposals
                .iter()
                .filter(|p| p.state == ProposalState::Active)
                .count() as u64,
            passed_proposals: proposals
                .iter()
                .filter(|p| p.state == ProposalState::Passed || p.state == ProposalState::Executed)
                .count() as u64,
            defeated_proposals: proposals
                .iter()
                .filter(|p| p.state == ProposalState::Defeated)
                .count() as u64,
            executed_proposals: proposals
                .iter()
                .filter(|p| p.state == ProposalState::Executed)
                .count() as u64,
            total_votes_cast: proposals.iter().map(|p| p.votes.len()).sum::<usize>() as u64,
            council_members: self.council.len(),
        }
    }
}

impl Default for DAO {
    fn default() -> Self {
        Self::new(VotingConfig::default())
    }
}

/// DAO statistics.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DaoStats {
    pub total_proposals: u64,
    pub active_proposals: u64,
    pub passed_proposals: u64,
    pub defeated_proposals: u64,
    pub executed_proposals: u64,
    pub total_votes_cast: u64,
    pub council_members: usize,
}

#[cfg(test)]
mod tests {
    use super::*;
    use synor_types::address::AddressType;

    fn test_address(n: u8) -> Address {
        let mut bytes = [0u8; 32];
        bytes[0] = n;
        Address::from_parts(synor_types::Network::Devnet, AddressType::P2PKH, bytes)
    }

    #[test]
    fn test_proposal_creation() {
        let mut dao = DAO::new(VotingConfig::fast());
        let proposer = test_address(1);

        let id = dao
            .create_proposal(
                proposer,
                1_000_000 * 100_000_000, // 1M tokens
                ProposalType::Signaling {
                    title: "Test".to_string(),
                    description: "Test proposal".to_string(),
                    options: vec!["Yes".to_string(), "No".to_string()],
                },
                "Test Proposal".to_string(),
                "This is a test".to_string(),
                100,
                70_000_000 * 100_000_000,
            )
            .unwrap();

        let proposal = dao.get_proposal(&id).unwrap();
        assert_eq!(proposal.state, ProposalState::Pending);
    }

    #[test]
    fn test_insufficient_voting_power() {
        let mut dao = DAO::new(VotingConfig::default());
        let proposer = test_address(1);

        let result = dao.create_proposal(
            proposer,
            100, // Not enough tokens
            ProposalType::Signaling {
                title: "Test".to_string(),
                description: "Test".to_string(),
                options: vec![],
            },
            "Test".to_string(),
            "Test".to_string(),
            100,
            70_000_000 * 100_000_000,
        );

        assert!(matches!(result, Err(DaoError::InsufficientVotingPower)));
    }

    #[test]
    fn test_voting_flow() {
        let mut dao = DAO::new(VotingConfig::fast());
        let proposer = test_address(1);
        let voter1 = test_address(2);
        let voter2 = test_address(3);

        // Create proposal
        let id = dao
            .create_proposal(
                proposer,
                1_000_000 * 100_000_000,
                ProposalType::Signaling {
                    title: "Test".to_string(),
                    description: "Test".to_string(),
                    options: vec![],
                },
                "Test".to_string(),
                "Test".to_string(),
                0,
                70_000_000 * 100_000_000,
            )
            .unwrap();

        // Try voting before start
        let result = dao.vote(&id, voter1.clone(), 100_000, VoteChoice::Yes, 5, None);
        assert!(matches!(result, Err(DaoError::VotingNotStarted)));

        // Vote after start
        dao.vote(
            &id,
            voter1.clone(),
            1_000_000_000,
            VoteChoice::Yes,
            15,
            None,
        )
        .unwrap();
        dao.vote(&id, voter2, 500_000_000, VoteChoice::No, 16, None)
            .unwrap();

        let proposal = dao.get_proposal(&id).unwrap();
        assert_eq!(proposal.votes.len(), 2);
        assert_eq!(proposal.yes_votes, 1_000_000_000);
        assert_eq!(proposal.no_votes, 500_000_000);
    }

    #[test]
    fn test_quadratic_voting() {
        let power = VotingPower::Quadratic(1_000_000);
        let weight = power.weight();
        assert_eq!(weight, 1000); // sqrt(1_000_000) = 1000

        let power2 = VotingPower::Quadratic(4);
        assert_eq!(power2.weight(), 2); // sqrt(4) = 2
    }

    #[test]
    fn test_proposal_passes() {
        let mut dao = DAO::new(VotingConfig::fast());
        let proposer = test_address(1);

        let id = dao
            .create_proposal(
                proposer.clone(),
                1_000_000 * 100_000_000,
                ProposalType::Signaling {
                    title: "Test".to_string(),
                    description: "Test".to_string(),
                    options: vec![],
                },
                "Test".to_string(),
                "Test".to_string(),
                0,
                100 * 100_000_000, // Small total supply for easy quorum
            )
            .unwrap();

        // Vote with more than quorum
        dao.vote(&id, proposer, 10 * 100_000_000, VoteChoice::Yes, 15, None)
            .unwrap();

        // Update state after voting ends
        dao.update_all_states(200);

        let proposal = dao.get_proposal(&id).unwrap();
        assert_eq!(proposal.state, ProposalState::Passed);
    }

    #[test]
    fn test_guardian_cancel() {
        let mut dao = DAO::new(VotingConfig::fast());
        let proposer = test_address(1);
        let guardian = test_address(99);

        dao.set_guardian(guardian.clone());

        let id = dao
            .create_proposal(
                proposer,
                1_000_000 * 100_000_000,
                ProposalType::Signaling {
                    title: "Test".to_string(),
                    description: "Test".to_string(),
                    options: vec![],
                },
                "Test".to_string(),
                "Test".to_string(),
                0,
                70_000_000 * 100_000_000,
            )
            .unwrap();

        // Guardian cancels
        dao.cancel(&id, &guardian).unwrap();

        let proposal = dao.get_proposal(&id).unwrap();
        assert_eq!(proposal.state, ProposalState::Cancelled);
    }
}
